Skip to content

Commit

Permalink
Merge pull request #186 from raydak-labs/fix/issue-183
Browse files Browse the repository at this point in the history
fix: do gracefully stop instance processing if an error occurs.
  • Loading branch information
BlackDark authored Feb 5, 2025
2 parents 7866bcd + 35d0c30 commit 1771573
Show file tree
Hide file tree
Showing 15 changed files with 140 additions and 36 deletions.
3 changes: 3 additions & 0 deletions docs/docs/configuration/_include/config-file-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ sonarr:
api_key: !secret SONARR_API_KEY # Reference to API key in secrets.yml
# api_key: !env SONARR_API_KEY # load from environment variable

# since v1.11.0. You can disable instances. Optional
# enabled: false

quality_definition:
type: series # Quality definition type for Sonarr

Expand Down
6 changes: 6 additions & 0 deletions docs/docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ radarrEnabled: false
whisparrEnabled: false
readarrEnabled: false
lidarrEnabled: false
# You can also disable on per instance basis
sonarr:
instance1:
# ...
enabled: false
```

## Quality Definition / Size
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Each variable can be set to customize the behavior of the application.
| Variable Name | Default Value | Required | Description |
| -------------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------- |
| `LOG_LEVEL` | `"info"` | No | Sets the logging level. Options are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. |
| `LOG_STACKTRACE` | `"false"` | No | (Experimental, v1.11.0) Outputs additionally stacktraces of underlying errors. |
| `CONFIG_LOCATION` | `"./config/config.yml"` | No | Specifies the path to the configuration file. |
| `SECRETS_LOCATION` | `"./config/secrets.yml"` | No | Specifies the path to the secrets file. |
| `CUSTOM_REPO_ROOT` | `"./repos"` | No | Defines the root directory for custom repositories. |
Expand All @@ -23,6 +24,7 @@ Each variable can be set to customize the behavior of the application.
| `LOAD_LOCAL_SAMPLES` | `"false"` | No | If `"true"`, loads local sample data for testing purposes. |
| `DEBUG_CREATE_FILES` | `"false"` | No | Enables debugging for file creation processes when set to `"true"`. |
| `TZ` | `"Etc/UTC"` | No | Timezone for the container. |
| `STOP_ON_ERROR` | `"false"` | No | (Experimental, v1.11.0) Stop execution on any error on any instance. |

## Usage

Expand Down
3 changes: 3 additions & 0 deletions examples/full/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ sonarr:
base_url: http://sonarr:8989
api_key: !secret SONARR_API_KEY

# since v1.11.0, optional, for disabling instances
enabled: false

quality_definition:
type: series

Expand Down
40 changes: 30 additions & 10 deletions src/__generated__/ky-client.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/clients/lidarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
QualityProfileResource,
} from "../__generated__/lidarr/data-contracts";
import { logger } from "../logger";
import { IArrClient, validateClientParams } from "./unified-client";
import { IArrClient, logConnectionError, validateClientParams } from "./unified-client";

export class LidarrClient implements IArrClient<QualityProfileResource, QualityDefinitionResource, CustomFormatResource, LanguageResource> {
private api!: Api<unknown>;
Expand Down Expand Up @@ -112,7 +112,9 @@ export class LidarrClient implements IArrClient<QualityProfileResource, QualityD
try {
await this.api.v1HealthList();
} catch (error) {
logger.error(error);
const message = logConnectionError(error, "LIDARR");
logger.error(message);

return false;
}

Expand Down
5 changes: 3 additions & 2 deletions src/clients/radarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "../__generated__/radarr/data-contracts";
import { logger } from "../logger";
import { cloneWithJSON } from "../util";
import { IArrClient, validateClientParams } from "./unified-client";
import { IArrClient, logConnectionError, validateClientParams } from "./unified-client";

export class RadarrClient implements IArrClient<QualityProfileResource, QualityDefinitionResource, CustomFormatResource, LanguageResource> {
private api!: Api<unknown>;
Expand Down Expand Up @@ -112,7 +112,8 @@ export class RadarrClient implements IArrClient<QualityProfileResource, QualityD
try {
await this.api.v3HealthList();
} catch (error) {
logger.error(error);
const message = logConnectionError(error, "RADARR");
logger.error(message);
return false;
}

Expand Down
5 changes: 3 additions & 2 deletions src/clients/readarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
QualityProfileResource,
} from "../__generated__/readarr/data-contracts";
import { logger } from "../logger";
import { IArrClient, validateClientParams } from "./unified-client";
import { IArrClient, logConnectionError, validateClientParams } from "./unified-client";

export class ReadarrClient
implements IArrClient<QualityProfileResource, QualityDefinitionResource, CustomFormatResource, LanguageResource>
Expand Down Expand Up @@ -114,7 +114,8 @@ export class ReadarrClient
try {
await this.api.v1HealthList();
} catch (error) {
logger.error(error);
const message = logConnectionError(error, "READARR");
logger.error(message);
return false;
}

Expand Down
5 changes: 3 additions & 2 deletions src/clients/sonarr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
QualityProfileResource,
} from "../__generated__/sonarr/data-contracts";
import { logger } from "../logger";
import { IArrClient, validateClientParams } from "./unified-client";
import { IArrClient, logConnectionError, validateClientParams } from "./unified-client";

export type SonarrQualityProfileResource = {
id?: number;
Expand Down Expand Up @@ -104,7 +104,8 @@ export class SonarrClient implements IArrClient<QualityProfileResource, QualityD
try {
await this.api.v3HealthList();
} catch (error) {
logger.error(error);
const message = logConnectionError(error, "SONARR");
logger.error(message);
return false;
}

Expand Down
16 changes: 10 additions & 6 deletions src/clients/unified-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@ export const validateClientParams = (url: string, apiKey: string, arrType: ArrTy
}
};

export const handleErrorApi = (error: any, arrType: ArrType) => {
export const logConnectionError = (error: any, arrType: ArrType) => {
let message;
const arrLabel = arrType.toLowerCase();
const causeError = error?.cause?.message || error?.cause?.errors?.map((e: any) => e.message).join(";") || undefined;

const errorMessage = (error.message && `Message: ${error.message}`) || "";
const causeMessage = (causeError && `- Cause: ${causeError}`) || "";

logger.error(`Error configuring ${arrLabel} API. ${errorMessage} ${causeMessage}`);

if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
Expand All @@ -54,18 +52,24 @@ export const handleErrorApi = (error: any, arrType: ArrType) => {
message = `An unexpected error occurred while setting up the ${arrLabel} request: ${errorMessage} ${causeMessage}. Please try again.`;
}

throw new Error(message);
return message;
};

export const configureApi = async (type: ArrType, baseUrl: string, apiKey: string) => {
unsetApi();

unifiedClient = new UnifiedClient(type, baseUrl, apiKey);
let connectionSuccessful = false;

try {
await unifiedClient.testConnection();
connectionSuccessful = await unifiedClient.testConnection();
} catch (error: any) {
handleErrorApi(error, type);
logger.error(`Unhandled connection error.`);
throw error;
}

if (!connectionSuccessful) {
throw new Error(`Could not connect to client: ${type} - ${baseUrl}`);
}

return unifiedClient;
Expand Down
5 changes: 3 additions & 2 deletions src/clients/whisparr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "../__generated__/whisparr/data-contracts";
import { logger } from "../logger";
import { cloneWithJSON } from "../util";
import { IArrClient, validateClientParams } from "./unified-client";
import { IArrClient, logConnectionError, validateClientParams } from "./unified-client";

/**
* Overwrite wrong types for now
Expand Down Expand Up @@ -122,7 +122,8 @@ export class WhisparrClient
try {
await this.api.v3HealthList();
} catch (error) {
logger.error(error);
const message = logConnectionError(error, "WHISPARR");
logger.error(message);
return false;
}

Expand Down
12 changes: 12 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ const schema = z.object({
.transform((x) => x === "true")
.pipe(z.boolean())
.default("false"),
STOP_ON_ERROR: z
.string()
.toLowerCase()
.transform((x) => x === "true")
.pipe(z.boolean())
.default("false"),
LOG_STACKTRACE: z
.string()
.toLowerCase()
.transform((x) => x === "true")
.pipe(z.boolean())
.default("false"),
});

// declare global {
Expand Down
60 changes: 50 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ServerCache } from "./cache";
import { configureApi, getUnifiedClient, unsetApi } from "./clients/unified-client";
import { getConfig, mergeConfigsAndTemplates } from "./config";
import { calculateCFsToManage, loadCustomFormatDefinitions, loadServerCustomFormats, manageCf } from "./custom-formats";
import { logHeading, logger } from "./logger";
import { logHeading, logInstanceHeading, logger } from "./logger";
import { calculateMediamanagementDiff, calculateNamingDiff } from "./media-management";
import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./quality-definitions";
import { calculateQualityProfilesDiff, loadQualityProfilesFromServer } from "./quality-profiles";
Expand Down Expand Up @@ -177,18 +177,48 @@ const runArrType = async (
globalConfig: InputConfigSchema,
arrEntry: Record<string, InputConfigArrInstance> | undefined,
) => {
const status = {
success: 0,
failure: 0,
skipped: 0,
};

if (arrEntry == null || Array.isArray(arrEntry) || typeof arrEntry !== "object" || Object.keys(arrEntry).length <= 0) {
logHeading(`No ${arrType} instances defined.`);
} else {
logHeading(`Processing ${arrType} ...`);

for (const [instanceName, instance] of Object.entries(arrEntry)) {
logger.info(`Processing ${arrType} Instance: ${instanceName}`);
await configureApi(arrType, instance.base_url, instance.api_key);
await pipeline(globalConfig, instance, arrType);
unsetApi();
logInstanceHeading(`Processing ${arrType} Instance: ${instanceName} ...`);

if (instance.enabled === false) {
logger.info(`Instance ${arrType} - ${instanceName} is disabled!`);
status.skipped++;
continue;
}

try {
await configureApi(arrType, instance.base_url, instance.api_key);
await pipeline(globalConfig, instance, arrType);
status.success++;
} catch (err: unknown) {
logger.error(`Failure during configuring: ${arrType} - ${instanceName}`);
status.failure++;
if (getEnvs().LOG_STACKTRACE) {
logger.error(err);
}
if (getEnvs().STOP_ON_ERROR) {
throw new Error(`Stopping further execution because 'STOP_ON_ERROR' is enabled.`);
}
} finally {
unsetApi();
}

logger.info("");
}
}

return status;
};

const run = async () => {
Expand All @@ -203,40 +233,50 @@ const run = async () => {

// TODO currently this has to be run sequentially because of the centrally configured api

const totalStatus: string[] = [];

// Sonarr
if (globalConfig.sonarrEnabled == null || globalConfig.sonarrEnabled) {
await runArrType("SONARR", globalConfig, globalConfig.sonarr);
const result = await runArrType("SONARR", globalConfig, globalConfig.sonarr);
totalStatus.push(`SONARR: (${result.success}/${result.failure}/${result.skipped})`);
} else {
logger.debug(`Sonarr disabled in config`);
}

// Radarr
if (globalConfig.radarrEnabled == null || globalConfig.radarrEnabled) {
await runArrType("RADARR", globalConfig, globalConfig.radarr);
const result = await runArrType("RADARR", globalConfig, globalConfig.radarr);
totalStatus.push(`RADARR: (${result.success}/${result.failure}/${result.skipped})`);
} else {
logger.debug(`Radarr disabled in config`);
}

// Whisparr
if (globalConfig.whisparrEnabled == null || globalConfig.whisparrEnabled) {
await runArrType("WHISPARR", globalConfig, globalConfig.whisparr);
const result = await runArrType("WHISPARR", globalConfig, globalConfig.whisparr);
totalStatus.push(`WHISPARR: (${result.success}/${result.failure}/${result.skipped})`);
} else {
logger.debug(`Whisparr disabled in config`);
}

// Readarr
if (globalConfig.readarrEnabled == null || globalConfig.readarrEnabled) {
await runArrType("READARR", globalConfig, globalConfig.readarr);
const result = await runArrType("READARR", globalConfig, globalConfig.readarr);
totalStatus.push(`READARR: (${result.success}/${result.failure}/${result.skipped})`);
} else {
logger.debug(`Readarr disabled in config`);
}

// Lidarr
if (globalConfig.lidarrEnabled == null || globalConfig.lidarrEnabled) {
await runArrType("LIDARR", globalConfig, globalConfig.lidarr);
const result = await runArrType("LIDARR", globalConfig, globalConfig.lidarr);
totalStatus.push(`LIDARR: (${result.success}/${result.failure}/${result.skipped})`);
} else {
logger.debug(`Lidarr disabled in config`);
}

logger.info(``);
logger.info(`Execution Summary (success/failure/skipped) instances: ${totalStatus.join(" - ")}`);
};

run();
4 changes: 4 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,9 @@ export const logHeading = (title: string) => {
logger.info("");
};

export const logInstanceHeading = (title: string) => {
logger.info(`### ${title}`);
};

// For debugging how envs have been loaded
logger.debug({ envs: getEnvs(), helpers: getHelpers() }, `Loaded following configuration from ENVs and mapped`);
Loading

0 comments on commit 1771573

Please sign in to comment.