Skip to content

Commit

Permalink
feat(config): adds timeLimit
Browse files Browse the repository at this point in the history
- New timeLimit option for interrupting scans even if there are repositories left
- Optional value with default of 5h30m
  • Loading branch information
AriPerkkio committed Dec 16, 2020
1 parent 1e02166 commit 5cb133d
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 17 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ module.exports = {
/** Optional boolean flag used to enable caching of cloned repositories. For CIs it's ideal to disable caching. Defauls to true. */
cache: true,

/** Optional time limit in seconds for the scan. Scan is interrupted after reaching the limit. Defaults to 5 hours 30 minutes. */
timeLimit: 5.5 * 60 * 60, // 5 hours 30 minutes

/**
* Optional callback invoked once scan is complete.
*
Expand Down
1 change: 1 addition & 0 deletions ci/base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
concurrentTasks: 3,
logLevel: process.env.CI ? 'info' : 'verbose',
cache: process.env.CI ? false : true,
timeLimit: 5.9 * 60 * 60,
CI: true,
eslintrc: {
root: true,
Expand Down
3 changes: 3 additions & 0 deletions lib/config/config-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const CONFIGURATION_FILE_TEMPLATE =
/** Optional boolean flag used to enable caching of cloned repositories. For CIs it's ideal to disable caching. Defauls to true. */
cache: true,
/** Optional time limit in seconds for the scan. Scan is interrupted after reaching the limit. Defaults to 5 hours 30 minutes. */
timeLimit: 5.5 * 60 * 60, // 5 hours 30 minutes
/**
* Optional callback invoked once scan is complete.
*
Expand Down
1 change: 1 addition & 0 deletions lib/config/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface Config {
CI: boolean;
logLevel: LogLevel;
cache: boolean;
timeLimit: number;
onComplete?: (results: ResultTemplateOptions[]) => Promise<void> | void;
}

Expand Down
8 changes: 8 additions & 0 deletions lib/config/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const DEFAULT_RESULT_PARSER_CI: ResultParser = 'plaintext';
const DEFAULT_LOG_LEVEL: LogLevel = 'verbose';
const DEFAULT_CONCURRENT_TASKS = 5;
const DEFAULT_MAX_FILE_SIZE_BYTES = 2000000;
const DEFAULT_TIME_LIMIT_SECONDS = 5.5 * 60 * 60;

const UNKNOWN_RULE_REGEXP = /^Definition for rule (.*) was not found.$/;

Expand All @@ -36,6 +37,7 @@ export default function constructAndValidateConfiguration(
CI,
logLevel,
cache,
timeLimit,
onComplete,
...unknownKeys
} = configToValidate;
Expand Down Expand Up @@ -142,6 +144,12 @@ export default function constructAndValidateConfiguration(
config.concurrentTasks = DEFAULT_CONCURRENT_TASKS;
}

if (timeLimit != null && typeof timeLimit !== 'number') {
errors.push(`timeLimit (${timeLimit}) should be a number.`);
} else if (timeLimit == null) {
config.timeLimit = DEFAULT_TIME_LIMIT_SECONDS;
}

if (onComplete && typeof onComplete !== 'function') {
errors.push(`onComplete (${onComplete}) should be a function`);
}
Expand Down
30 changes: 21 additions & 9 deletions lib/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import workerTask, { WorkerMessage, createErrorMessage } from './worker-task';
import { LintMessage } from './types';
import { resolveConfigurationLocation } from '@config';

type WorkerCallback<T> = (worker: Worker) => T;
type CleanupCallback = () => void;
type EffectCallback = WorkerCallback<CleanupCallback>;

if (!isMainThread) {
workerTask();
}
Expand All @@ -14,7 +18,8 @@ if (!isMainThread) {
*/
function scanRepository(
repository: string,
onMessage: (message: WorkerMessage) => void
onMessage: (message: WorkerMessage) => void,
workerCallback: EffectCallback
): Promise<LintMessage[]> {
return new Promise(resolve => {
// Notify about worker starting. It can take a while to get worker starting up
Expand All @@ -37,6 +42,8 @@ function scanRepository(
},
});

const cleanup = workerCallback(worker);

worker.on('message', (message: WorkerMessage) => {
switch (message.type) {
case 'LINT_END':
Expand All @@ -63,15 +70,20 @@ function scanRepository(
});

worker.on('exit', code => {
if (code !== 0) {
resolve([
createErrorMessage({
message: `Worker exited with code ${code}`,
path: repository,
ruleId: '',
}),
]);
cleanup();

// 0 = success, 1 = termination
if (code === 0 || code === 1) {
return resolve([]);
}

resolve([
createErrorMessage({
message: `Worker exited with code ${code}`,
path: repository,
ruleId: '',
}),
]);
});
});
}
Expand Down
19 changes: 16 additions & 3 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { renderApplication } from '@ui';
import config, { validateEslintrcRules } from '@config';
import engine, { WorkerMessage } from '@engine';
import engine from '@engine';
import { writeResults, clearResults } from '@file-client';
import logger from '@progress-logger';

Expand All @@ -17,7 +17,7 @@ async function main() {
async function execute(): Promise<void> {
const task = pool.shift();

if (task) {
if (task && !logger.isTimeout()) {
await task();
return execute();
}
Expand Down Expand Up @@ -49,7 +49,7 @@ async function main() {
async function scanRepo(repository: string) {
const results = await engine.scanRepository(
repository,
(message: WorkerMessage) => {
function onMessage(message) {
switch (message.type) {
case 'START':
return logger.onTaskStart(repository);
Expand Down Expand Up @@ -98,6 +98,19 @@ async function scanRepo(repository: string) {
case 'DEBUG':
return;
}
},
// On scan timeout terminate all on-going workers
function workerCallback(worker) {
function onTimeout() {
// This will start the termination asynchronously. It is enough
// for timeout use case and doesn't require awaiting for finishing.
worker.terminate();
}
logger.on('timeout', onTimeout);

return function cleanup() {
logger.off('timeout', onTimeout);
};
}
);

Expand Down
20 changes: 20 additions & 0 deletions lib/progress-logger/log-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import { CACHE_LOCATION } from '@file-client';
// Regexp for converting `[INFO][LINTING] reponame` to `[INFO/LINTING] reponame`
const CI_TEMPLATE_TASK_REGEXP = /\]\[/;

/**
* Format seconds into display format, e.g. 36092 -> 10h 1m 32s
*/
function formatSeconds(timeSeconds: number): string {
const hours = Math.floor(timeSeconds / 3600);
const minutes = Math.floor((timeSeconds % 3600) / 60);
const seconds = Math.floor(timeSeconds % 60);

return [
hours && `${hours}h`,
minutes && `${minutes}m`,
seconds && `${seconds}s`,
]
.filter(Boolean)
.join(' ');
}

export const TASK_TEMPLATE = (task: Task): string => {
switch (task.step) {
case 'START':
Expand Down Expand Up @@ -91,5 +108,8 @@ export const OVERFLOWING_ROWS_TOP = (overflowingRowCount: number): string =>
export const OVERFLOWING_ROWS_BOTTOM = (overflowingRowCount: number): string =>
`[\u25BC to see ${overflowingRowCount} lines below]`;

export const SCAN_TIMELIMIT_REACHED = (timeSeconds: number): string =>
`[DONE] Reached scan time limit ${formatSeconds(timeSeconds)}`;

export const SCAN_FINISHED = (scannedRepositories: number): string =>
`[DONE] Finished scan of ${scannedRepositories} repositories`;
41 changes: 39 additions & 2 deletions lib/progress-logger/progress-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,16 @@ class ProgressLogger {
message: [],
task: [],
ciKeepAlive: [],
timeout: [],
};

/** Interval of CI status messages. Used to avoid CIs timeouting. */
/** Indicates whether scan has reached time limit set by `config.timeLimit` */
private hasTimedout = false;

/** Handle of scan timeout. Used to interrupt scan once time limit has been reached. */
private scanTimeoutHandle: NodeJS.Timeout | null = null;

/** Interval of CI status messages. Used to avoid CIs timeouting due to silent stdout. */
private ciKeepAliveIntervalHandle: NodeJS.Timeout | null = null;

constructor() {
Expand All @@ -78,6 +85,10 @@ class ProgressLogger {
this.onCiStatus();
}, CI_KEEP_ALIVE_INTERVAL_MS);
}

this.scanTimeoutHandle = setTimeout(() => {
this.onScanTimeout();
}, config.timeLimit * 1000);
}

/**
Expand Down Expand Up @@ -121,10 +132,20 @@ class ProgressLogger {
}
}

/**
* Get current log messages
*/
getMessages(): LogMessage[] {
return this.messages.filter(message => isLogVisible(message));
}

/**
* Check whether scan has timed out
*/
isTimeout(): boolean {
return this.hasTimedout;
}

/**
* Add final message and fire exit event
*/
Expand All @@ -135,11 +156,14 @@ class ProgressLogger {
level: 'verbose',
});

// Stop CI messages
if (this.ciKeepAliveIntervalHandle !== null) {
clearInterval(this.ciKeepAliveIntervalHandle);
}

if (this.scanTimeoutHandle !== null) {
clearTimeout(this.scanTimeoutHandle);
}

const onError = (error: Error) =>
console.error(
[
Expand Down Expand Up @@ -409,6 +433,19 @@ class ProgressLogger {
this.listeners.ciKeepAlive.forEach(listener => listener(message));
}
}

/**
* Log notification about reaching scan time limit and notify listeners
*/
private onScanTimeout() {
this.addNewMessage({
content: Templates.SCAN_TIMELIMIT_REACHED(config.timeLimit),
level: 'info',
color: 'yellow',
});
this.hasTimedout = true;
this.listeners.timeout.forEach(listener => listener());
}
}

export default new ProgressLogger();
2 changes: 2 additions & 0 deletions lib/progress-logger/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export type Listener<Key = ListenerType> =
Key extends 'task' ? (task: Task, done?: boolean) => void :
Key extends 'exit' ? () => void :
Key extends 'ciKeepAlive' ? (message: string) => void :
Key extends 'timeout' ? () => void :
never;

export interface Listeners {
exit: Listener<'exit'>[];
message: Listener<'message'>[];
task: Listener<'task'>[];
ciKeepAlive: Listener<'ciKeepAlive'>[];
timeout: Listener<'timeout'>[];
}

export type ListenerType = keyof Listeners;
38 changes: 35 additions & 3 deletions test/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { ConfigToValidate } from '@config/types';
const DEFAULT_CONFIGURATION: ConfigToValidate = {
repositories: ['test-repo', 'test-repo-2'],
extensions: ['.ts', '.tsx'],
rulesUnderTesting: [],
eslintrc: {},
pathIgnorePattern: undefined,
maxFileSizeBytes: undefined,
rulesUnderTesting: [],
resultParser: 'plaintext',
resultParser: undefined,
concurrentTasks: undefined,
eslintrc: {},
CI: undefined,
cache: undefined,
logLevel: undefined,
timeLimit: undefined,
onComplete: undefined,
};

describe('Config validator', () => {
Expand Down Expand Up @@ -52,6 +54,36 @@ describe('Config validator', () => {
);
});

test('default values are set in CI mode', () => {
const config = validator({ ...DEFAULT_CONFIGURATION, CI: true });

expect(config.resultParser).toBe('plaintext');
expect(config.logLevel).toBe('verbose');
expect(config.concurrentTasks).toBe(5);
expect(config.maxFileSizeBytes).toBe(2e6);
expect(config.timeLimit).toBe(5.5 * 60 * 60);
expect(config.pathIgnorePattern).toBe(undefined);
expect(config.onComplete).toBe(undefined);
});

test('default values are set in CLI mode', () => {
const config = validator({ ...DEFAULT_CONFIGURATION, CI: false });

expect(config.resultParser).toBe('markdown');
expect(config.logLevel).toBe('verbose');
expect(config.concurrentTasks).toBe(5);
expect(config.maxFileSizeBytes).toBe(2e6);
expect(config.timeLimit).toBe(5.5 * 60 * 60);
expect(config.pathIgnorePattern).toBe(undefined);
expect(config.onComplete).toBe(undefined);
});

test('default value for CI is set', () => {
const config = validator(DEFAULT_CONFIGURATION);

expect(config.CI).toBe(process.env.CI === 'true');
});

test('additional options are unsupported', () => {
const key = 'someMistypedKey';
const config = { ...DEFAULT_CONFIGURATION, [key]: true };
Expand Down

0 comments on commit 5cb133d

Please sign in to comment.