Skip to content

fix(logs): close race that dropped appends in -f follow mode#79

Merged
Rinse12 merged 1 commit into
masterfrom
fix/logs-follow-append-race
Jun 9, 2026
Merged

fix(logs): close race that dropped appends in -f follow mode#79
Rinse12 merged 1 commit into
masterfrom
fix/logs-follow-append-race

Conversation

@Rinse12

@Rinse12 Rinse12 commented Jun 9, 2026

Copy link
Copy Markdown
Member

Context

Follow-up to #78. That PR merged the test hardening + diagnostics, but the actual root-cause fix was pushed to the branch after #78 had already merged, so it never reached master. This PR carries just that fix.

Root cause (proven, not guessed)

The diagnostics added in #78 fired on the windows CI job and I then reproduced the failure locally under CPU saturation. bitsocial logs -f set its starting byte offset from a separate fsPromise.stat() taken after the initial readFile + parse + stdout write:

const existingContent = await readFile(file)   // reads N bytes, prints them
...parse/filter/write to stdout...             // <- an append can land here
const stat = await stat(file)                  // size now N+M
let position = stat.size                         // = N+M, skips the M new bytes

Any append landing between the readFile and the stat is silently dropped: those bytes were never in the printed dump, and position jumps past them, so the 300ms poll loop never re-reads them. Under load the window between the two awaits widens — which is why the logs -f test failed intermittently on all three platforms. The follow-trace showed position==observedSize (whole file incl. the append) from the first poll with bytesThisCycle=0 forever.

This also affected real users: a log line appended during logs -f startup could be intermittently missed.

Fix

Anchor position to Buffer.byteLength(existingContent) — the exact bytes just read — before any further awaits. Anything appended afterward is at offset >= position and is picked up by the poll. No separate stat, no race window.

Verification

  • 20/20 green under 48-process CPU saturation that previously reproduced the drop within ~2 runs.
  • Full logs.test.ts (29 tests) passes.

Refs #77

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue in the logs command's follow/tail mode where some log entries could be skipped during rapid file updates.

Root cause (proven by the test diagnostics + local reproduction under CPU
saturation): follow mode set its starting byte offset from a SEPARATE
fsPromise.stat() taken AFTER the initial readFile + parse + stdout write:

    const existingContent = await readFile(file)   // reads N bytes, prints them
    ...parse/filter/write to stdout...             // append can land here
    const stat = await stat(file)                  // size now N+M
    let position = stat.size                        // = N+M, skips the M new bytes

Any append landing between the readFile and the stat is silently dropped: the
new bytes were not in the dumped content, and position jumps past them so the
poll loop (which reads from position) never re-reads them. Under load the window
between the two awaits widens, which is why CI failed intermittently on all
three platforms with the appended line never appearing in stdout.

The follow-trace diagnostics showed position==observedSize==81 (full file
including the append) from the very first poll, with bytesThisCycle=0 forever —
position had already been advanced past the unread append.

Fix: anchor position to Buffer.byteLength of the exact content we just read,
before any further awaits. Anything appended afterwards is at offset >= position
and is picked up by the poll. Verified 20/20 green under 48-process CPU
saturation that previously reproduced the drop within ~2 runs.

Refs #77
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The follow-mode initialization in the logs command now anchors its byte position directly to the already-read content's length instead of issuing a separate stat call, eliminating a race where file appends occurring between the read and stat would be silently skipped.

Changes

Follow-mode byte offset race condition fix

Layer / File(s) Summary
Fix follow-mode byte offset race
src/cli/commands/logs.ts
Position initialization for follow-mode tailing now uses the byte length of existingContent instead of fsPromise.stat().size, ensuring appends between the initial read and offset calculation are not lost.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • bitsocialnet/bitsocial-cli#43: Both PRs modify follow-mode tailing in logs.ts to avoid appends being skipped by anchoring the initial position read differently for subsequent streaming.
  • bitsocialnet/bitsocial-cli#78: The main PR fixes the follow-mode byte-offset anchoring in logs.ts, while the retrieved PR hardens follow-mode tests to avoid CI timing flakiness—both improve the same --follow behavior.

Poem

A rabbit hops through byte streams swift,
No more the stat call's racy drift—
Content length now holds the place,
Where tails resume without a race.
Follow-mode runs pure and clean,
The finest logs you've ever seen! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(logs): close race that dropped appends in -f follow mode' directly and specifically describes the main change: fixing a race condition in the logs follow mode that was dropping appended content.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/logs-follow-append-race

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/cli/commands/logs.ts

Oops! Something went wrong! :(

ESLint: 8.27.0

Error: ESLint configuration in --config » eslint-config-oclif is invalid:

  • Unexpected top-level property "__esModule".

Referenced from: /.eslintrc
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2156:19)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2998:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2963:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3264:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3135:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3074:25)
at _normalizeObjectConfigDataBody.next ()
at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:20)
at _normalizeObjectConfigData.next ()
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2829:16)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/commands/logs.ts (1)

292-293: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rotation path still has the same read→stat skip race.

After reading and printing newContent, setting position from a later stat.size can skip bytes appended between those two awaits. Anchor position to bytes actually read (Buffer.byteLength(newContent, "utf-8")) to keep behavior consistent with the fix above.

Suggested patch
-                const newStat = await fsPromise.stat(currentLogFile);
-                position = newStat.size;
+                // Avoid read->stat race on rotated file initialization, same as initial follow setup
+                position = Buffer.byteLength(newContent, "utf-8");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/logs.ts` around lines 292 - 293, After reading and printing
newContent, don't reset position using a later
fsPromise.stat(currentLogFile).size (which can skip bytes added between the read
and stat); instead compute and assign position using the actual number of bytes
consumed from the file by setting position += Buffer.byteLength(newContent,
"utf-8") (use the same newContent variable and encoding), replacing the current
stat-based assignment that references fsPromise.stat and currentLogFile so
position reflects bytes read, not file size at a later async point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/cli/commands/logs.ts`:
- Around line 292-293: After reading and printing newContent, don't reset
position using a later fsPromise.stat(currentLogFile).size (which can skip bytes
added between the read and stat); instead compute and assign position using the
actual number of bytes consumed from the file by setting position +=
Buffer.byteLength(newContent, "utf-8") (use the same newContent variable and
encoding), replacing the current stat-based assignment that references
fsPromise.stat and currentLogFile so position reflects bytes read, not file size
at a later async point.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7876c2ec-40f1-46fd-bcc3-d4182def4c7c

📥 Commits

Reviewing files that changed from the base of the PR and between 9767222 and d0e5e40.

📒 Files selected for processing (1)
  • src/cli/commands/logs.ts

@Rinse12 Rinse12 merged commit 5d78cee into master Jun 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant