mirror of
https://github.com/unraid/api.git
synced 2026-01-01 06:01:18 -06:00
fix(connect): mothership connection (#1464)
--- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210709463978079 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved management of cloud connection status and error handling for DNS resolution issues. * Introduced a centralized controller for managing mothership connection lifecycle and subscriptions. * **Refactor** * Streamlined event handling and resource management for mothership connections. * Consolidated connection logic to enhance reliability and maintainability. * Optimized initialization process by deferring GraphQL client creation until needed. * **Chores** * Updated module configuration to include the new controller for better dependency management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -213,29 +213,36 @@ export class CloudService {
|
|||||||
resolve(hostname).then(([address]) => address),
|
resolve(hostname).then(([address]) => address),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!local.includes(network)) {
|
/**
|
||||||
// Question: should we actually throw an error, or just log a warning?
|
* If either resolver returns a private IP we still treat this as a fatal
|
||||||
//
|
* mis-configuration because the host will be unreachable from the public
|
||||||
// This is usually due to cloudflare's load balancing.
|
* Internet.
|
||||||
// if `dig +short mothership.unraid.net` shows both IPs, then this should be safe to ignore.
|
*
|
||||||
// this.logger.warn(
|
* The user likely has a PI-hole or something similar running that rewrites
|
||||||
// `Local and network resolvers showing different IP for "${hostname}". [local="${
|
* the record to a private address.
|
||||||
// local ?? 'NOT FOUND'
|
*/
|
||||||
// }"] [network="${network ?? 'NOT FOUND'}"].`
|
if (ip.isPrivate(local) || ip.isPrivate(network)) {
|
||||||
// );
|
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Local and network resolvers showing different IP for "${hostname}". [local="${
|
`"${hostname}" is being resolved to a private IP. [local="${local ?? 'NOT FOUND'}"] [network="${
|
||||||
local ?? 'NOT FOUND'
|
network ?? 'NOT FOUND'
|
||||||
}"] [network="${network ?? 'NOT FOUND'}"]`
|
}"]`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user likely has a PI-hole or something similar running.
|
/**
|
||||||
if (ip.isPrivate(local))
|
* Different public IPs are expected when Cloudflare (or anycast) load-balancing
|
||||||
throw new Error(
|
* is in place. Log the mismatch for debugging purposes but do **not** treat it
|
||||||
`"${hostname}" is being resolved to a private IP. [IP=${local ?? 'NOT FOUND'}]`
|
* as an error.
|
||||||
|
*
|
||||||
|
* It does not affect whether the server can connect to Mothership.
|
||||||
|
*/
|
||||||
|
if (local !== network) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Local and network resolvers returned different IPs for "${hostname}". [local="${local ?? 'NOT FOUND'}"] [network="${
|
||||||
|
network ?? 'NOT FOUND'
|
||||||
|
}"]`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return { local, network };
|
return { local, network };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export class MothershipGraphqlClientService implements OnModuleInit, OnModuleDes
|
|||||||
* Initialize the GraphQL client when the module is created
|
* Initialize the GraphQL client when the module is created
|
||||||
*/
|
*/
|
||||||
async onModuleInit(): Promise<void> {
|
async onModuleInit(): Promise<void> {
|
||||||
await this.createClientInstance();
|
|
||||||
this.configService.getOrThrow('API_VERSION');
|
this.configService.getOrThrow('API_VERSION');
|
||||||
this.configService.getOrThrow('MOTHERSHIP_GRAPHQL_LINK');
|
this.configService.getOrThrow('MOTHERSHIP_GRAPHQL_LINK');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TimeoutCheckerJob } from '../connection-status/timeout-checker.job.js';
|
||||||
|
import { MothershipConnectionService } from './connection.service.js';
|
||||||
|
import { MothershipGraphqlClientService } from './graphql.client.js';
|
||||||
|
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for (starting and stopping) the mothership stack:
|
||||||
|
* - GraphQL client (to mothership)
|
||||||
|
* - Subscription handler (websocket communication with mothership)
|
||||||
|
* - Timeout checker (to detect if the connection to mothership is lost)
|
||||||
|
* - Connection service (controller for connection state & metadata)
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MothershipController implements OnModuleDestroy, OnApplicationBootstrap {
|
||||||
|
private readonly logger = new Logger(MothershipController.name);
|
||||||
|
constructor(
|
||||||
|
private readonly clientService: MothershipGraphqlClientService,
|
||||||
|
private readonly connectionService: MothershipConnectionService,
|
||||||
|
private readonly subscriptionHandler: MothershipSubscriptionHandler,
|
||||||
|
private readonly timeoutCheckerJob: TimeoutCheckerJob
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
await this.initOrRestart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the mothership stack. Throws on first error.
|
||||||
|
*/
|
||||||
|
async stop() {
|
||||||
|
this.timeoutCheckerJob.stop();
|
||||||
|
this.subscriptionHandler.stopMothershipSubscription();
|
||||||
|
await this.clientService.clearInstance();
|
||||||
|
this.connectionService.resetMetadata();
|
||||||
|
this.subscriptionHandler.clearAllSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to stop, then starts the mothership stack. Throws on first error.
|
||||||
|
*/
|
||||||
|
async initOrRestart() {
|
||||||
|
await this.stop();
|
||||||
|
const { state } = this.connectionService.getIdentityState();
|
||||||
|
this.logger.verbose('cleared, got identity state');
|
||||||
|
if (!state.apiKey) {
|
||||||
|
this.logger.warn('No API key found; cannot setup mothership subscription');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.clientService.createClientInstance();
|
||||||
|
await this.subscriptionHandler.subscribeToMothershipEvents();
|
||||||
|
this.timeoutCheckerJob.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,44 @@
|
|||||||
import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { PubSub } from 'graphql-subscriptions';
|
import { PubSub } from 'graphql-subscriptions';
|
||||||
|
|
||||||
import { MinigraphStatus } from '../config/connect.config.js';
|
import { MinigraphStatus } from '../config/connect.config.js';
|
||||||
import { TimeoutCheckerJob } from '../connection-status/timeout-checker.job.js';
|
|
||||||
import { EVENTS, GRAPHQL_PUBSUB_CHANNEL, GRAPHQL_PUBSUB_TOKEN } from '../helper/nest-tokens.js';
|
import { EVENTS, GRAPHQL_PUBSUB_CHANNEL, GRAPHQL_PUBSUB_TOKEN } from '../helper/nest-tokens.js';
|
||||||
import { MothershipConnectionService } from './connection.service.js';
|
import { MothershipConnectionService } from './connection.service.js';
|
||||||
import { MothershipGraphqlClientService } from './graphql.client.js';
|
import { MothershipController } from './mothership.controller.js';
|
||||||
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MothershipHandler implements OnModuleDestroy {
|
export class MothershipHandler {
|
||||||
private readonly logger = new Logger(MothershipHandler.name);
|
private readonly logger = new Logger(MothershipHandler.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly connectionService: MothershipConnectionService,
|
private readonly connectionService: MothershipConnectionService,
|
||||||
private readonly clientService: MothershipGraphqlClientService,
|
private readonly mothershipController: MothershipController,
|
||||||
private readonly subscriptionHandler: MothershipSubscriptionHandler,
|
|
||||||
private readonly timeoutCheckerJob: TimeoutCheckerJob,
|
|
||||||
@Inject(GRAPHQL_PUBSUB_TOKEN)
|
@Inject(GRAPHQL_PUBSUB_TOKEN)
|
||||||
private readonly legacyPubSub: PubSub
|
private readonly legacyPubSub: PubSub
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
|
||||||
await this.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
this.timeoutCheckerJob.stop();
|
|
||||||
this.subscriptionHandler.stopMothershipSubscription();
|
|
||||||
await this.clientService.clearInstance();
|
|
||||||
this.connectionService.resetMetadata();
|
|
||||||
this.subscriptionHandler.clearAllSubscriptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setup() {
|
|
||||||
await this.clear();
|
|
||||||
const { state } = this.connectionService.getIdentityState();
|
|
||||||
this.logger.verbose('cleared, got identity state');
|
|
||||||
if (!state.apiKey) {
|
|
||||||
this.logger.warn('No API key found; cannot setup mothership subscription');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.clientService.createClientInstance();
|
|
||||||
await this.subscriptionHandler.subscribeToMothershipEvents();
|
|
||||||
this.timeoutCheckerJob.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnEvent(EVENTS.IDENTITY_CHANGED, { async: true })
|
@OnEvent(EVENTS.IDENTITY_CHANGED, { async: true })
|
||||||
async onIdentityChanged() {
|
async onIdentityChanged() {
|
||||||
const { state } = this.connectionService.getIdentityState();
|
const { state } = this.connectionService.getIdentityState();
|
||||||
if (state.apiKey) {
|
if (state.apiKey) {
|
||||||
this.logger.verbose('Identity changed; setting up mothership subscription');
|
this.logger.verbose('Identity changed; setting up mothership subscription');
|
||||||
await this.setup();
|
await this.mothershipController.initOrRestart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent(EVENTS.MOTHERSHIP_CONNECTION_STATUS_CHANGED, { async: true })
|
@OnEvent(EVENTS.MOTHERSHIP_CONNECTION_STATUS_CHANGED, { async: true })
|
||||||
async onMothershipConnectionStatusChanged() {
|
async onMothershipConnectionStatusChanged() {
|
||||||
const state = this.connectionService.getConnectionState();
|
const state = this.connectionService.getConnectionState();
|
||||||
// Question: do we include MinigraphStatus.ERROR_RETRYING here?
|
if (
|
||||||
if (state && [MinigraphStatus.PING_FAILURE].includes(state.status)) {
|
state &&
|
||||||
|
[MinigraphStatus.PING_FAILURE, MinigraphStatus.ERROR_RETRYING].includes(state.status)
|
||||||
|
) {
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
'Mothership connection status changed to %s; setting up mothership subscription',
|
'Mothership connection status changed to %s; setting up mothership subscription',
|
||||||
state.status
|
state.status
|
||||||
);
|
);
|
||||||
await this.setup();
|
await this.mothershipController.initOrRestart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +57,6 @@ export class MothershipHandler implements OnModuleDestroy {
|
|||||||
await this.legacyPubSub.publish(GRAPHQL_PUBSUB_CHANNEL.OWNER, {
|
await this.legacyPubSub.publish(GRAPHQL_PUBSUB_CHANNEL.OWNER, {
|
||||||
owner: { username: 'root', url: '', avatar: '' },
|
owner: { username: 'root', url: '', avatar: '' },
|
||||||
});
|
});
|
||||||
this.timeoutCheckerJob.stop();
|
await this.mothershipController.stop();
|
||||||
await this.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { MothershipConnectionService } from './connection.service.js';
|
|||||||
import { MothershipGraphqlClientService } from './graphql.client.js';
|
import { MothershipGraphqlClientService } from './graphql.client.js';
|
||||||
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
|
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
|
||||||
import { MothershipHandler } from './mothership.events.js';
|
import { MothershipHandler } from './mothership.events.js';
|
||||||
|
import { MothershipController } from './mothership.controller.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RemoteAccessModule],
|
imports: [RemoteAccessModule],
|
||||||
@@ -23,6 +24,7 @@ import { MothershipHandler } from './mothership.events.js';
|
|||||||
TimeoutCheckerJob,
|
TimeoutCheckerJob,
|
||||||
CloudService,
|
CloudService,
|
||||||
CloudResolver,
|
CloudResolver,
|
||||||
|
MothershipController,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user