-
-
Notifications
You must be signed in to change notification settings - Fork 11
fix: stop the OOM/502 outage at the root — PDS restore guard, bounded native memory #197
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
2248fe6
fix: stop the OOM/502 outage at the root — PDS restore guard, bounded…
nperez0111 28ebebf
refactor: drop the server-side OG cache — Cloudflare already is the c…
nperez0111 516ca25
docs: condense AGENTS.md — fix inaccuracies, cut postmortem narratives
nperez0111 fdabf65
chore: track unused google.ts and isbndb.ts scrapers
nperez0111 7fe4cd4
fix: address review — 304s on ebook downloads, breaker leak, FTS afte…
nperez0111 81e0029
docs: correct the scraper section — google.ts and isbndb.ts are back
nperez0111 d66d4fc
perf: skip the main-DB VACUUM when there is nothing to reclaim
nperez0111 362bf68
fix: end leaked otel spans, trim every author link, order the KV sweep
nperez0111 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,6 @@ | ||
| [test] | ||
| root = "src" | ||
| # Was "src", which silently made server/ untestable — server/worker-exit.ts | ||
| # shipped a bug that mislabelled every OOM kill for a month. Scripts pass the | ||
| # directories explicitly (`bun test src server`) so app/'s React Native suites, | ||
| # which use the jest preset rather than bun's runner, stay out of the way. | ||
| root = "." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { describe, it, expect } from "bun:test"; | ||
| import { classifyWorkerExit, readProcessMemoryKb, signalName } from "./worker-exit.ts"; | ||
|
|
||
| describe("signalName", () => { | ||
| // The bug: Bun passes the signal *name*, its types claim a number, and the | ||
| // number-keyed lookup fell through to `SIG${code}` — producing "SIGSIGKILL" | ||
| // in production for every one of 148 OOM kills. | ||
| it("passes through a name Bun already prefixed", () => { | ||
| expect(signalName("SIGKILL")).toBe("SIGKILL"); | ||
| expect(signalName("SIGTERM")).toBe("SIGTERM"); | ||
| }); | ||
|
|
||
| it("still maps a numeric code, in case Bun's types become honest", () => { | ||
| expect(signalName(9)).toBe("SIGKILL"); | ||
| expect(signalName(15)).toBe("SIGTERM"); | ||
| }); | ||
|
|
||
| it("is null for a clean exit", () => { | ||
| expect(signalName(null)).toBeNull(); | ||
| expect(signalName(undefined)).toBeNull(); | ||
| }); | ||
|
|
||
| it("prefixes a bare name", () => { | ||
| expect(signalName("KILL")).toBe("SIGKILL"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("classifyWorkerExit", () => { | ||
| it("flags a cgroup OOM kill", () => { | ||
| const event = classifyWorkerExit({ | ||
| index: 1, | ||
| pid: 4242, | ||
| exitCode: null, | ||
| signalCode: "SIGKILL", | ||
| uptimeMs: 775_653, | ||
| }); | ||
|
|
||
| expect(event.signal).toBe("SIGKILL"); | ||
| expect(event.likely_oom).toBe(true); | ||
| expect(event.msg).toBe("worker_exit"); | ||
| expect(event.level).toBe(50); | ||
| expect(event.worker).toBe(1); | ||
| expect(event.pid).toBe(4242); | ||
| expect(event.uptime_ms).toBe(775_653); | ||
| }); | ||
|
|
||
| it("does not flag a graceful exit as an OOM", () => { | ||
| const event = classifyWorkerExit({ | ||
| index: 0, | ||
| exitCode: 0, | ||
| signalCode: null, | ||
| uptimeMs: 1_000, | ||
| }); | ||
| expect(event.signal).toBeNull(); | ||
| expect(event.likely_oom).toBe(false); | ||
| }); | ||
|
|
||
| it("does not flag SIGTERM as an OOM", () => { | ||
| const event = classifyWorkerExit({ | ||
| index: 0, | ||
| exitCode: null, | ||
| signalCode: "SIGTERM", | ||
| uptimeMs: 1_000, | ||
| }); | ||
| expect(event.likely_oom).toBe(false); | ||
| }); | ||
|
|
||
| it("does not flag a SIGKILL that carried an exit code", () => { | ||
| // Something other than the kernel's OOM killer produced this. | ||
| const event = classifyWorkerExit({ | ||
| index: 0, | ||
| exitCode: 137, | ||
| signalCode: "SIGKILL", | ||
| uptimeMs: 1_000, | ||
| }); | ||
| expect(event.likely_oom).toBe(false); | ||
| }); | ||
|
|
||
| it("carries the last memory sample so a kill is attributable", () => { | ||
| const event = classifyWorkerExit({ | ||
| index: 2, | ||
| exitCode: null, | ||
| signalCode: "SIGKILL", | ||
| uptimeMs: 700_000, | ||
| memory: { rss_kb: 2_580_000, anon_kb: 1_779_772 }, | ||
| }); | ||
| expect(event.anon_kb).toBe(1_779_772); | ||
| expect(event.rss_kb).toBe(2_580_000); | ||
| }); | ||
|
|
||
| it("omits memory fields entirely when there is no sample", () => { | ||
| const event = classifyWorkerExit({ | ||
| index: 0, | ||
| exitCode: null, | ||
| signalCode: "SIGKILL", | ||
| uptimeMs: 1, | ||
| memory: null, | ||
| }); | ||
| expect("anon_kb" in event).toBe(false); | ||
| expect("rss_kb" in event).toBe(false); | ||
| }); | ||
|
|
||
| it("serializes to a single JSON log line", () => { | ||
| const line = JSON.parse( | ||
| JSON.stringify( | ||
| classifyWorkerExit({ | ||
| index: 1, | ||
| exitCode: null, | ||
| signalCode: "SIGKILL", | ||
| uptimeMs: 5, | ||
| }), | ||
| ), | ||
| ); | ||
| expect(line.msg).toBe("worker_exit"); | ||
| expect(line.signal).toBe("SIGKILL"); | ||
| expect(line.likely_oom).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("readProcessMemoryKb", () => { | ||
| const SMAPS = [ | ||
| "Rss: 1624780 kB", | ||
| "Pss: 1019518 kB", | ||
| "Shared_Clean: 1028192 kB", | ||
| "Private_Dirty: 554892 kB", | ||
| "Anonymous: 554892 kB", | ||
| ].join("\n"); | ||
|
|
||
| it("parses Rss and Anonymous out of smaps_rollup", () => { | ||
| expect(readProcessMemoryKb(1, () => SMAPS)).toEqual({ | ||
| rss_kb: 1_624_780, | ||
| anon_kb: 554_892, | ||
| }); | ||
| }); | ||
|
|
||
| it("returns null off Linux, where procfs does not exist", () => { | ||
| expect( | ||
| readProcessMemoryKb(1, () => { | ||
| throw new Error("ENOENT"); | ||
| }), | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for output with neither field", () => { | ||
| expect(readProcessMemoryKb(1, () => "Pss: 12 kB")).toBeNull(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: nperez0111/bookhive
Length of output: 9227
🏁 Script executed:
Repository: nperez0111/bookhive
Length of output: 308
🏁 Script executed:
Repository: nperez0111/bookhive
Length of output: 4942
Tie server startup to OpenObserve readiness.
servertargetshttp://openobserve:5080, butopenobservehas no healthcheck andserverdoes not depend on it;otel-collectorusesdependentso it can start beforeserverandopenobserveare both running. Add a healthcheck toopenobserve:latestand makeserverdepend onopenobserve.service_healthy, or use an explicit startup health-check wrapper, so logs and traces are not configured against a service that is too early to accept connections.🤖 Prompt for AI Agents