-
Notifications
You must be signed in to change notification settings - Fork 8
feat(mongodb-log-writer): add retentionGB
configuration MONGOSH-1985
#504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
b638b34
6e4e929
bd2e5f9
b008854
99afade
9f3c1bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,6 @@ import { ObjectId } from 'bson'; | |
import { once } from 'events'; | ||
import { createWriteStream, promises as fs } from 'fs'; | ||
import { createGzip, constants as zlibConstants } from 'zlib'; | ||
import { Heap } from 'heap-js'; | ||
import { MongoLogWriter } from './mongo-log-writer'; | ||
import { Writable } from 'stream'; | ||
|
||
|
@@ -17,9 +16,11 @@ interface MongoLogOptions { | |
retentionDays: number; | ||
/** The maximal number of log files which are kept. */ | ||
maxLogFileCount?: number; | ||
/** A handler for warnings related to a specific filesystem path. */ | ||
onerror: (err: Error, path: string) => unknown | Promise<void>; | ||
/** The maximal GB of log files which are kept. */ | ||
retentionGB?: number; | ||
/** A handler for errors related to a specific filesystem path. */ | ||
onerror: (err: Error, path: string) => unknown | Promise<void>; | ||
/** A handler for warnings related to a specific filesystem path. */ | ||
onwarn: (err: Error, path: string) => unknown | Promise<void>; | ||
} | ||
|
||
|
@@ -38,9 +39,37 @@ export class MongoLogManager { | |
/** Clean up log files older than `retentionDays`. */ | ||
async cleanupOldLogFiles(maxDurationMs = 5_000): Promise<void> { | ||
const dir = this._options.directory; | ||
let dirHandle; | ||
const sortedLogFiles: { | ||
fullPath: string; | ||
id: string; | ||
size?: number; | ||
}[] = []; | ||
let usedStorageSize = this._options.retentionGB ? 0 : -Infinity; | ||
|
||
try { | ||
dirHandle = await fs.opendir(dir); | ||
const files = await fs.readdir(dir, { withFileTypes: true }); | ||
for (const file of files) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh yeah, I originally had a |
||
const { id } = | ||
/^(?<id>[a-f0-9]{24})_log(\.gz)?$/i.exec(file.name)?.groups ?? {}; | ||
|
||
if (!file.isFile() || !id) { | ||
continue; | ||
} | ||
|
||
const fullPath = path.join(dir, file.name); | ||
let size: number | undefined; | ||
if (this._options.retentionGB) { | ||
try { | ||
size = (await fs.stat(fullPath)).size; | ||
addaleax marked this conversation as resolved.
Show resolved
Hide resolved
|
||
usedStorageSize += size; | ||
} catch (err) { | ||
this._options.onerror(err as Error, fullPath); | ||
continue; | ||
} | ||
} | ||
|
||
sortedLogFiles.push({ fullPath, id, size }); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This being a loop means we should be checking It's also a bit unfortunate that if this part takes longer than the maximum duration, we now don't end up deleting files even if we have already identified that they should be deleted regardless of that (e.g. through the expiration time setting or the max file count setting) |
||
} catch { | ||
return; | ||
} | ||
|
@@ -49,48 +78,56 @@ export class MongoLogManager { | |
// Delete files older than N days | ||
const deletionCutoffTimestamp = | ||
deletionStartTimestamp - this._options.retentionDays * 86400 * 1000; | ||
// Store the known set of least recent files in a heap in order to be able to | ||
// delete all but the most recent N files. | ||
const leastRecentFileHeap = new Heap<{ | ||
fileTimestamp: number; | ||
fullPath: string; | ||
}>((a, b) => a.fileTimestamp - b.fileTimestamp); | ||
|
||
for await (const dirent of dirHandle) { | ||
const storageSizeLimit = this._options.retentionGB | ||
? this._options.retentionGB * 1024 * 1024 * 1024 | ||
: Infinity; | ||
|
||
for await (const { id, fullPath } of [...sortedLogFiles]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused – Where is |
||
// Cap the overall time spent inside this function. Consider situations like | ||
// a large number of machines using a shared network-mounted $HOME directory | ||
// where lots and lots of log files end up and filesystem operations happen | ||
// with network latency. | ||
if (Date.now() - deletionStartTimestamp > maxDurationMs) break; | ||
|
||
if (!dirent.isFile()) continue; | ||
const { id } = | ||
/^(?<id>[a-f0-9]{24})_log(\.gz)?$/i.exec(dirent.name)?.groups ?? {}; | ||
if (!id) continue; | ||
const fileTimestamp = +new ObjectId(id).getTimestamp(); | ||
const fullPath = path.join(dir, dirent.name); | ||
let toDelete: string | undefined; | ||
let toDelete: | ||
| { | ||
fullPath: string; | ||
/** If the file wasn't deleted right away and there is a | ||
* retention size limit, its size should be accounted */ | ||
fileSize?: number; | ||
} | ||
| undefined; | ||
|
||
// If the file is older than expected, delete it. If the file is recent, | ||
// add it to the list of seen files, and if that list is too large, remove | ||
// the least recent file we've seen so far. | ||
if (fileTimestamp < deletionCutoffTimestamp) { | ||
toDelete = fullPath; | ||
} else if (this._options.maxLogFileCount) { | ||
leastRecentFileHeap.push({ fullPath, fileTimestamp }); | ||
if (leastRecentFileHeap.size() > this._options.maxLogFileCount) { | ||
toDelete = leastRecentFileHeap.pop()?.fullPath; | ||
toDelete = { | ||
fullPath, | ||
}; | ||
} else if (this._options.retentionGB || this._options.maxLogFileCount) { | ||
const reachedMaxStorageSize = usedStorageSize > storageSizeLimit; | ||
const reachedMaxFileCount = | ||
this._options.maxLogFileCount && | ||
sortedLogFiles.length > this._options.maxLogFileCount; | ||
|
||
if (reachedMaxStorageSize || reachedMaxFileCount) { | ||
toDelete = sortedLogFiles.shift(); | ||
gagik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
if (!toDelete) continue; | ||
try { | ||
await fs.unlink(toDelete); | ||
await fs.unlink(toDelete.fullPath); | ||
if (toDelete.fileSize) { | ||
usedStorageSize -= toDelete.fileSize; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (err: any) { | ||
if (err?.code !== 'ENOENT') { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument | ||
this._options.onerror(err, fullPath); | ||
this._options.onerror(err as Error, fullPath); | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.