Skip to content

Commit 38f280e

Browse files
ikreymertw4l
andauthored
Interrupt instead of fail crawl when not fatal (#973)
- Add logger.interrupt() which results in crawler being restarted, but without failing the crawl, unlike logger.fatal() - Switch a few logger.fatal() -> logger.interrupt(), when crawl could be retried, only use fatal() when initial crawl config is invalid for the most part - Don't increment 'failing' and set to 'failed' after unexpected errors, set to interrupted as well to allow for retry (and exponential backoff if in k8s) - Allow logger to manage opening/closing its open external log file, via setOutputFile() and closeLog() - Move closeLog() and setStatusAndExit() to logger itself so logger.interrupt() and logger.fatal() can call these - Use the newer logger.interrupt() for: proxy connection unavailable, out of disk space, interrupt signal and WACZ upload failed --------- Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
1 parent b6076ad commit 38f280e

6 files changed

Lines changed: 88 additions & 77 deletions

File tree

docs/docs/user-guide/cli-options.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@ Options:
9595
errors, debug
9696
[array] [default: ["stats"]]
9797
--logLevel Comma-separated list of log levels t
98-
o include in logs
99-
[array] [default: []]
98+
o include in logs. By default all bu
99+
t debug are included. To include deb
100+
ug messages in logs, debug must also
101+
be passed to logging option.
102+
[array] [choices: "debug", "info", "warn", "error", "interrupt", "fatal"] [def
103+
ault: []]
100104
--context, --logContext Comma-separated list of contexts to
101105
include in logs
102106
[array] [choices: "general", "worker", "recorder", "recorderNetwork", "writer"

src/crawler.ts

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,7 @@ import {
6464
import { Recorder } from "./util/recorder.js";
6565
import { SitemapReader } from "./util/sitemapper.js";
6666
import { ScopedSeed, parseSeeds } from "./util/seeds.js";
67-
import {
68-
WARCWriter,
69-
createWARCInfo,
70-
setWARCInfo,
71-
streamFinish,
72-
} from "./util/warcwriter.js";
67+
import { WARCWriter, createWARCInfo, setWARCInfo } from "./util/warcwriter.js";
7368
import { isHTMLMime, isRedirectStatus } from "./util/reqresp.js";
7469
import { initProxy } from "./util/proxy.js";
7570
import { initFlow, nextFlowStep } from "./util/flowbehavior.js";
@@ -123,7 +118,6 @@ export class Crawler {
123118

124119
pagesFH?: WriteStream | null = null;
125120
extraPagesFH?: WriteStream | null = null;
126-
logFH: WriteStream | null = null;
127121

128122
crawlId: string;
129123

@@ -482,7 +476,7 @@ export class Crawler {
482476

483477
async bootstrap() {
484478
if (await isDiskFull(this.params.cwd)) {
485-
logger.fatal(
479+
logger.interrupt(
486480
"Out of disk space, exiting",
487481
{},
488482
"general",
@@ -512,8 +506,7 @@ export class Crawler {
512506

513507
await fsp.mkdir(this.downloadsDir, { recursive: true });
514508

515-
this.logFH = fs.createWriteStream(this.logFilename, { flags: "a" });
516-
logger.setExternalLogStream(this.logFH);
509+
logger.setOutputFile(this.logFilename);
517510

518511
this.infoString = await getInfoString();
519512
setWARCInfo(this.infoString, this.params.warcInfo);
@@ -678,14 +671,11 @@ export class Crawler {
678671
exitCode = ExitCodes.Failed;
679672
}
680673
} catch (e) {
681-
logger.error("Crawl failed", e);
682-
exitCode = ExitCodes.Failed;
683-
status = "failing";
684-
if (await this.crawlState.incFailCount()) {
685-
status = "failed";
686-
}
674+
logger.error("Unexpected error, interrupting", e);
675+
exitCode = ExitCodes.GenericError;
676+
status = "interrupted";
687677
} finally {
688-
await this.setStatusAndExit(exitCode, status);
678+
await logger.setStatusAndExit(exitCode, status);
689679
}
690680
}
691681

@@ -1677,7 +1667,7 @@ self.__bx_behaviors.selectMainBehavior();
16771667
async checkCanceled() {
16781668
if (this.crawlState && (await this.crawlState.isCrawlCanceled())) {
16791669
await this.cleanupOnCancel();
1680-
await this.setStatusAndExit(ExitCodes.Success, "canceled");
1670+
await logger.setStatusAndExit(ExitCodes.Success, "canceled");
16811671
return true;
16821672
}
16831673

@@ -1690,17 +1680,6 @@ self.__bx_behaviors.selectMainBehavior();
16901680
}
16911681
}
16921682

1693-
async setStatusAndExit(exitCode: ExitCodes, status: string) {
1694-
logger.info(`Exiting, Crawl status: ${status}`);
1695-
1696-
await this.closeLog();
1697-
1698-
if (this.crawlState && status) {
1699-
await this.crawlState.setStatus(status);
1700-
}
1701-
process.exit(exitCode);
1702-
}
1703-
17041683
async serializeAndExit() {
17051684
const initState = await this.crawlState.getStatus();
17061685
// don't do anything if already post-processing!
@@ -1716,15 +1695,17 @@ self.__bx_behaviors.selectMainBehavior();
17161695
await this.closeFiles();
17171696

17181697
if (!this.done) {
1719-
await this.setStatusAndExit(
1698+
logger.interrupt(
1699+
"Forced interrupt by signal",
1700+
{},
1701+
"general",
17201702
ExitCodes.SignalInterruptedForce,
1721-
"interrupted",
17221703
);
17231704
return true;
17241705
}
17251706
}
17261707

1727-
await this.setStatusAndExit(ExitCodes.Success, "done");
1708+
await logger.setStatusAndExit(ExitCodes.Success, "done");
17281709
return true;
17291710
}
17301711

@@ -2064,17 +2045,6 @@ self.__bx_behaviors.selectMainBehavior();
20642045
this.browser.crashed = true;
20652046
}
20662047

2067-
async closeLog(): Promise<void> {
2068-
// close file-based log
2069-
logger.setExternalLogStream(null);
2070-
if (!this.logFH) {
2071-
return;
2072-
}
2073-
const logFH = this.logFH;
2074-
this.logFH = null;
2075-
await streamFinish(logFH);
2076-
}
2077-
20782048
async generateWACZ(): Promise<WACZ | null> {
20792049
logger.info("Generating WACZ");
20802050
await this.crawlState.setStatus("generate-wacz");
@@ -2095,7 +2065,7 @@ self.__bx_behaviors.selectMainBehavior();
20952065
if ((await this.crawlState.numDone()) > 0) {
20962066
return null;
20972067
}
2098-
// fail crawl otherwise
2068+
// interrupt crawl otherwise
20992069
logger.fatal("No WARC Files, assuming crawl failed");
21002070
}
21012071

@@ -2111,7 +2081,7 @@ self.__bx_behaviors.selectMainBehavior();
21112081

21122082
logger.debug("End of log file in WACZ, storing logs to WACZ file");
21132083

2114-
await this.closeLog();
2084+
await logger.closeLog();
21152085

21162086
const requires = await this.crawlState.getDupeDependentCrawls();
21172087

@@ -2156,12 +2126,12 @@ self.__bx_behaviors.selectMainBehavior();
21562126
}
21572127
return wacz;
21582128
} catch (e) {
2159-
logger.error("Error creating WACZ", e);
2160-
if (!streaming) {
2161-
logger.fatal("Unable to write WACZ successfully");
2162-
} else if (this.params.restartsOnError) {
2163-
await this.setStatusAndExit(ExitCodes.UploadFailed, "interrupted");
2164-
}
2129+
logger.interrupt(
2130+
"Error creating / uploading WACZ",
2131+
formatErr(e),
2132+
"wacz",
2133+
ExitCodes.UploadFailed,
2134+
);
21652135
}
21662136

21672137
return null;

src/util/argParser.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ import { screenshotTypes } from "./screenshots.js";
2525
import {
2626
DEFAULT_EXCLUDE_LOG_CONTEXTS,
2727
LOG_CONTEXT_TYPES,
28+
LOG_LEVEL_TYPES,
2829
LogContext,
30+
LogLevel,
2931
logger,
3032
} from "./logger.js";
3133
import { SaveState } from "./state.js";
3234
import { loadProxyConfig } from "./proxy.js";
3335

3436
// ============================================================================
3537
export type CrawlerArgs = ReturnType<typeof parseArgs> & {
38+
logLevel: LogLevel[];
3639
logContext: LogContext[];
3740
logExcludeContext: LogContext[];
3841
text: string[];
@@ -277,9 +280,11 @@ class ArgParser {
277280
},
278281

279282
logLevel: {
280-
describe: "Comma-separated list of log levels to include in logs",
283+
describe:
284+
"Comma-separated list of log levels to include in logs. By default all but debug are included. To include debug messages in logs, debug must also be passed to logging option.",
281285
type: "array",
282286
default: [],
287+
choices: LOG_LEVEL_TYPES,
283288
coerce,
284289
},
285290

src/util/logger.ts

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import { Writable } from "node:stream";
55
import { RedisCrawlState } from "./state.js";
66
import { ExitCodes } from "./constants.js";
7+
import { streamFinish } from "./warcwriter.js";
8+
import fs from "node:fs";
79

810
// RegExp.prototype.toJSON = RegExp.prototype.toString;
911
Object.defineProperty(RegExp.prototype, "toJSON", {
@@ -63,7 +65,16 @@ export const LOG_CONTEXT_TYPES = [
6365

6466
export type LogContext = (typeof LOG_CONTEXT_TYPES)[number];
6567

66-
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
68+
export const LOG_LEVEL_TYPES = [
69+
"debug",
70+
"info",
71+
"warn",
72+
"error",
73+
"interrupt",
74+
"fatal",
75+
] as const;
76+
77+
export type LogLevel = (typeof LOG_LEVEL_TYPES)[number];
6778

6879
export const DEFAULT_EXCLUDE_LOG_CONTEXTS: LogContext[] = [
6980
"recorderNetwork",
@@ -77,7 +88,7 @@ class Logger {
7788
debugLogging = false;
7889
logErrorsToRedis = false;
7990
logBehaviorsToRedis = false;
80-
logLevels: string[] = [];
91+
logLevels: LogLevel[] = [];
8192
contexts: LogContext[] = [];
8293
excludeContexts: LogContext[] = [];
8394
defaultLogContext: LogContext = "general";
@@ -92,8 +103,8 @@ class Logger {
92103
this.fatalExitCode = exitCode;
93104
}
94105

95-
setExternalLogStream(logFH: Writable | null) {
96-
this.logStream = logFH;
106+
setOutputFile(filename: string) {
107+
this.logStream = fs.createWriteStream(filename, { flags: "a" });
97108
}
98109

99110
setDebugLogging(debugLog: boolean) {
@@ -108,7 +119,7 @@ class Logger {
108119
this.logBehaviorsToRedis = logBehaviorsToRedis;
109120
}
110121

111-
setLogLevel(logLevels: string[]) {
122+
setLogLevel(logLevels: LogLevel[]) {
112123
this.logLevels = logLevels;
113124
}
114125

@@ -168,7 +179,7 @@ class Logger {
168179
//
169180
}
170181

171-
const redisErrorLogLevels = ["error", "fatal"];
182+
const redisErrorLogLevels = ["error", "interrupt", "fatal"];
172183
if (
173184
this.logErrorsToRedis &&
174185
this.crawlState &&
@@ -225,21 +236,51 @@ class Logger {
225236
}
226237
}
227238

239+
interrupt(
240+
message: string,
241+
data = {},
242+
context: LogContext,
243+
exitCode: ExitCodes,
244+
) {
245+
this.logAsJSON(
246+
`${message}. Interrupting, can restart`,
247+
data,
248+
context,
249+
"interrupt",
250+
);
251+
252+
void this.setStatusAndExit(exitCode, "interrupted");
253+
}
254+
228255
fatal(
229256
message: string,
230257
data = {},
231258
context: LogContext = this.defaultLogContext,
232259
exitCode = ExitCodes.Success,
233260
) {
234-
exitCode = exitCode || this.fatalExitCode;
235261
this.logAsJSON(`${message}. Quitting`, data, context, "fatal");
236262

237-
if (this.crawlState) {
238-
this.crawlState
239-
.setStatus("failed")
240-
.catch(() => {})
241-
.finally(process.exit(exitCode));
242-
} else {
263+
void this.setStatusAndExit(exitCode || this.fatalExitCode, "interrupted");
264+
}
265+
266+
async closeLog() {
267+
if (this.logStream) {
268+
const logFH = this.logStream;
269+
this.logStream = null;
270+
await streamFinish(logFH);
271+
}
272+
}
273+
274+
async setStatusAndExit(exitCode: ExitCodes, status: string): Promise<void> {
275+
try {
276+
await this.closeLog();
277+
278+
if (this.crawlState && status) {
279+
await this.crawlState.setStatus(status);
280+
}
281+
} catch (e) {
282+
this.error("Error shutting down, exiting anyway", e);
283+
} finally {
243284
process.exit(exitCode);
244285
}
245286
}

src/util/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ export async function runSSHD(
433433
try {
434434
await waitForSocksPort;
435435
} catch (e) {
436-
logger.fatal(
436+
logger.interrupt(
437437
"Unable to establish SSH connection for proxy",
438438
{
439439
error: e,

src/util/state.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,15 +1238,6 @@ return inx;
12381238
});
12391239
}
12401240

1241-
async incFailCount() {
1242-
const key = `${this.crawlId}:status:failcount:${this.uid}`;
1243-
const res = await this.redis.incr(key);
1244-
1245-
// consider failed if 3 failed retries in 60 secs
1246-
await this.redis.expire(key, 60);
1247-
return res >= 3;
1248-
}
1249-
12501241
async addToQueue(
12511242
{
12521243
url,

0 commit comments

Comments
 (0)