Skip to content

in_tail: make stale database cleanup safe for shared databases - #12242

Open
Bing-Wang-derbysoft wants to merge 3 commits into
fluent:masterfrom
Bing-Wang-derbysoft:fix-tail-shared-db-cleanup
Open

in_tail: make stale database cleanup safe for shared databases#12242
Bing-Wang-derbysoft wants to merge 3 commits into
fluent:masterfrom
Bing-Wang-derbysoft:fix-tail-shared-db-cleanup

Conversation

@Bing-Wang-derbysoft

@Bing-Wang-derbysoft Bing-Wang-derbysoft commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Make Tail database stale-file cleanup operate on the database as a whole instead
of on the files discovered by each individual Tail input.

When multiple Tail inputs share one database, the previous startup cleanup could
delete valid offset records belonging to another input. After restarting Fluent
Bit, affected files could then be read again from the beginning.

The new cleanup:

  • runs once for the original shared SQLite context;
  • validates every stored path and inode directly against the filesystem;
  • removes records only when the path is missing or its inode has changed;
  • preserves records when filesystem validation fails for another reason;
  • retains valid offsets belonging to other Tail inputs.

Regression tests cover shared databases and replacing a file with a new inode at
the same path.

Fixes #8928.

Follow-up to #8062, which introduced startup cleanup of unmonitored database
entries. The affected behavior was released in Fluent Bit 3.0.2.


Testing

Example configuration

The shared database behavior was tested with two Tail inputs using the same
database:

[SERVICE]
    Flush              1
    Grace              1
    Log_Level          info

[INPUT]
    Name               tail
    Tag                shared.a
    Path               /tmp/a.log
    DB                 /tmp/tail.db
    DB.Sync            Full
    Read_from_Head     On
    Refresh_Interval   1

[INPUT]
    Name               tail
    Tag                shared.b
    Path               /tmp/b.log
    DB                 /tmp/tail.db
    DB.Sync            Full
    Read_from_Head     On
    Refresh_Interval   1

[OUTPUT]
    Name               stdout
    Match              shared.*
  • Example configuration file for the change
  • Debug log output from testing the change
  • Valgrind output showing no detected leaks or memory errors

Runtime tests

The runtime test binary and Fluent Bit executable were built in an Ubuntu 24.04
Docker environment.

./bin/flb-rt-in_tail \
  db \
  db_delete_stale_file \
  db_replaced_file_cleanup \
  db_shared_between_inputs \
  db_compare_filename \
  --no-color

Result:

SUCCESS: All unit tests have passed.

Integration tests

FLUENT_BIT_BINARY=/work/build/bin/fluent-bit \
  .venv/bin/python3 -m pytest \
  scenarios/in_tail/tests/test_in_tail_001.py::test_in_tail_restart_resumes_from_db_offset \
  scenarios/in_tail/tests/test_in_tail_001.py::test_in_tail_delete_and_recreate_same_path_is_reingested \
  -q

Result:

2 passed

The same integration scenarios were run under strict Valgrind validation:

VALGRIND=1 \
VALGRIND_STRICT=1 \
FLUENT_BIT_BINARY=/work/build/bin/fluent-bit \
  .venv/bin/python3 -m pytest \
  scenarios/in_tail/tests/test_in_tail_001.py::test_in_tail_restart_resumes_from_db_offset \
  scenarios/in_tail/tests/test_in_tail_001.py::test_in_tail_delete_and_recreate_same_path_is_reingested \
  -q

Result:

2 passed

VALGRIND_STRICT=1 checks the generated Valgrind logs for definite, indirect,
and possible leaks, as well as reported Valgrind errors.

Representative cleanup log output:

[ info] [input:tail:tail.0] db: stale file deleted from database: id=1
[ info] [input:tail:tail.0] db: cleaned stale file records: count=1

Amazon Linux 2023 manual verification

Additional functional testing was performed with:

/root/fbit/current/fluent-bit

The following scenarios passed:

  • a single input resumed from its stored offset after restart;
  • two inputs sharing a database read only newly appended content after restart;
  • a database record was removed after its file was deleted;
  • a new inode at the same path did not reuse the old offset;
  • starting one input did not remove another input's valid offset.

Packaging

This change does not modify packaging, containers, or native binary contents.

  • [N/A] Run local packaging tests showing all targets build
  • [N/A] Set the ok-package-test label

Documentation

This change does not add or modify configuration options or public interfaces.

  • [N/A] Documentation required for this feature

Backporting

This fixes behavior introduced by #8062 and released in Fluent Bit 3.0.2. It may
be suitable for backporting after the master PR is accepted.

  • Backport to latest stable release

Fluent Bit is licensed under Apache 2.0. By submitting this pull request, I
understand that this code will be released under the terms of that license.

Summary by CodeRabbit

  • Bug Fixes

    • Improved cleanup of stale file records after files are replaced or removed.
    • Preserved file offsets when monitored files are renamed.
    • Improved handling when multiple tail inputs share one database.
    • Prevented cleanup failures from blocking startup.
  • Tests

    • Added coverage for replacement, renaming, and shared-database scenarios.

Signed-off-by: Bing Wang <bing.wang@derbysoft.net>
Signed-off-by: Bing Wang <bing.wang@derbysoft.net>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The tail plugin moves SQLite stale-file cleanup to the pre-run callback. Cleanup checks file paths and inodes, preserves records monitored by matching inputs, and deletes records for missing files. Runtime tests cover replacement, rename, and shared-database scenarios.

Changes

Tail database cleanup

Layer / File(s) Summary
Filesystem-based cleanup contract and implementation
plugins/in_tail/tail_db.h, plugins/in_tail/tail_sql.h, plugins/in_tail/tail_db.c
The cleanup API now accepts an inode-monitoring callback. SQLite records are checked by path and inode. Missing-file records are deleted individually.
Pre-run cleanup wiring
plugins/in_tail/tail.c
Pre-run cleanup checks monitored inodes across matching tail inputs. Cleanup failures are logged without stopping startup.
Runtime regression coverage
tests/runtime/in_tail.c
Tests cover replacement cleanup, renamed-file offset preservation, and multiple tail inputs sharing one SQLite database.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Tail as Tail pre-run callback
  participant Cleanup as flb_tail_db_cleanup
  participant SQLite as SQLite database
  participant Filesystem as Filesystem
  Tail->>Cleanup: Check stale records
  Cleanup->>SQLite: Select file records
  SQLite-->>Cleanup: Return path and inode data
  Cleanup->>Filesystem: Stat stored paths
  Filesystem-->>Cleanup: Return file status and inode
  Cleanup->>SQLite: Delete records for missing files
  Cleanup-->>Tail: Return cleanup status
Loading

Possibly related PRs

Suggested reviewers: cosmo0920, edsiper

🚥 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 clearly summarizes the main change: safe stale database cleanup for shared in_tail databases.
Linked Issues check ✅ Passed The changes address issue #8928 by preserving valid offsets across Tail inputs that share a database.
Out of Scope Changes check ✅ Passed The code and regression tests remain within the shared-database cleanup objective and introduce no unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b22f9e0c2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread plugins/in_tail/tail_db.c

@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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/runtime/in_tail.c (2)

2932-2957: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report the reason the check failed.

Every failure mode returns FLB_FALSE: open failure, prepare failure, no rows, name mismatch, inode mismatch, and extra rows. The caller prints one generic message. A CI failure then gives no signal about which condition occurred.

Add TEST_MSG output at each failure point so the test log identifies the actual database state.

♻️ Proposed diagnostics
     ret = sqlite3_step(stmt);
     if (ret != SQLITE_ROW) {
+        TEST_MSG("no row found in in_tail_files, ret=%d", ret);
         goto cleanup;
     }
 
     name = sqlite3_column_text(stmt, 0);
     if (name == NULL || strcmp((const char *) name, file_path) != 0 ||
         sqlite3_column_int64(stmt, 1) != (sqlite3_int64) inode) {
+        TEST_MSG("row mismatch: name=%s expect=%s inode=%" PRId64
+                 " expect=%" PRIu64,
+                 name ? (const char *) name : "(null)", file_path,
+                 (int64_t) sqlite3_column_int64(stmt, 1), inode);
         goto cleanup;
     }
 
     ret = sqlite3_step(stmt);
     if (ret == SQLITE_DONE) {
         found = FLB_TRUE;
     }
+    else {
+        TEST_MSG("unexpected extra rows in in_tail_files");
+    }
🤖 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 `@tests/runtime/in_tail.c` around lines 2932 - 2957, The SQLite verification
logic should report the specific failure before each cleanup path instead of
returning only FLB_FALSE. Add TEST_MSG diagnostics in the sqlite3_open,
sqlite3_prepare_v2, initial sqlite3_step, name/inode validation, and final
sqlite3_step branches, including relevant return values or database-state
details where available; preserve the existing cleanup and found-result
behavior.

3181-3190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the database still holds both file records after the restart.

This test currently infers offset preservation from the output count. That is an indirect signal. The regression in issue #8928 is a database-layer defect: cleanup deleted the row owned by the other tail input.

Add a direct check that in_tail_files still contains two rows, one per file, after the second flb_start. The check pins the failure to the database layer and does not depend on flush timing.

tail_db_contains_file cannot be reused here because it requires exactly one row. Add a small row-count helper next to it.

♻️ Proposed helper and assertion
/* place next to tail_db_contains_file */
static int tail_db_count_files(const char *db_path)
{
    int count = -1;
    int ret;
    sqlite3 *db = NULL;
    sqlite3_stmt *stmt = NULL;

    ret = sqlite3_open(db_path, &db);
    if (ret != SQLITE_OK) {
        goto cleanup;
    }

    ret = sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM in_tail_files;",
                             -1, &stmt, NULL);
    if (ret != SQLITE_OK) {
        goto cleanup;
    }

    if (sqlite3_step(stmt) == SQLITE_ROW) {
        count = sqlite3_column_int(stmt, 0);
    }

cleanup:
    if (stmt != NULL) {
        sqlite3_finalize(stmt);
    }
    if (db != NULL) {
        sqlite3_close(db);
    }

    return count;
}
     flb_time_msleep(500);
     num = get_output_num();
     if (!TEST_CHECK(num == 2)) {
         TEST_MSG("num error. expect=2 got=%d", num);
     }
 
+    ret = tail_db_count_files(db);
+    if (!TEST_CHECK(ret == 2)) {
+        TEST_MSG("db record count error. expect=2 got=%d", ret);
+    }
+
     test_tail_ctx_destroy(ctx);
     unlink(db);
 }
🤖 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 `@tests/runtime/in_tail.c` around lines 3181 - 3190, Add a tail_db_count_files
helper next to tail_db_contains_file that queries COUNT(*) from in_tail_files
and safely cleans up SQLite resources. After the second flb_start and before
relying on output timing, assert that the helper returns exactly 2 rows,
preserving the existing output-count check as applicable.
🤖 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.

Inline comments:
In `@plugins/in_tail/tail_config.c`:
- Around line 476-480: Update the flb_tail_config_create cleanup path around
flb_tail_db_cleanup so a nonzero cleanup result is logged but does not destroy
ctx or return NULL. Continue tail input initialization after stale database
cleanup failures, preserving normal handling for successful cleanup.

---

Nitpick comments:
In `@tests/runtime/in_tail.c`:
- Around line 2932-2957: The SQLite verification logic should report the
specific failure before each cleanup path instead of returning only FLB_FALSE.
Add TEST_MSG diagnostics in the sqlite3_open, sqlite3_prepare_v2, initial
sqlite3_step, name/inode validation, and final sqlite3_step branches, including
relevant return values or database-state details where available; preserve the
existing cleanup and found-result behavior.
- Around line 3181-3190: Add a tail_db_count_files helper next to
tail_db_contains_file that queries COUNT(*) from in_tail_files and safely cleans
up SQLite resources. After the second flb_start and before relying on output
timing, assert that the helper returns exactly 2 rows, preserving the existing
output-count check as applicable.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e4c9478-76e3-4219-976a-4c8a47798c58

📥 Commits

Reviewing files that changed from the base of the PR and between fe293d4 and 3b22f9e.

📒 Files selected for processing (6)
  • plugins/in_tail/tail.c
  • plugins/in_tail/tail_config.c
  • plugins/in_tail/tail_db.c
  • plugins/in_tail/tail_db.h
  • plugins/in_tail/tail_sql.h
  • tests/runtime/in_tail.c
💤 Files with no reviewable changes (1)
  • plugins/in_tail/tail.c

Comment thread plugins/in_tail/tail_config.c Outdated
Signed-off-by: Bing Wang <bing.wang@derbysoft.net>

@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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/runtime/in_tail.c (1)

3146-3162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the database state and remove the renamed file at the end.

The test verifies the emitted record count but not the database content. The PR adds tail_db_contains_file. Use it after the restart to confirm that the record for the renamed path survived cleanup with the original inode. That check fails if cleanup deletes the record, which is the exact regression this PR fixes; the record-count check alone can pass for other reasons.

The test also leaves test_db_renamed.log.1 on disk. The file is removed at the start of the test, so reruns work, but removing it at the end keeps the working directory clean for other tests that use glob paths.

💚 Proposed test additions
     num = get_output_num();
     if (!TEST_CHECK(num == 1)) {
         TEST_MSG("num error after append. expect=1 got=%d", num);
     }
 
+    ret = tail_db_contains_file(db, renamed_file[0], inode);
+    if (!TEST_CHECK(ret == FLB_TRUE)) {
+        TEST_MSG("database record for the renamed file was not preserved");
+    }
+
     test_tail_ctx_destroy(ctx);
+    unlink(renamed_file[0]);
     unlink(db);
 }

Capture the inode with stat() before the rename to pass it to tail_db_contains_file.

🤖 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 `@tests/runtime/in_tail.c` around lines 3146 - 3162, Update the rename/restart
test around test_tail_ctx_destroy to capture the original file inode with stat()
before renaming, then call tail_db_contains_file after the restart to assert the
renamed path remains associated with that inode. Retain the existing
output-count assertions and unlink test_db_renamed.log.1 during final cleanup.
plugins/in_tail/tail_db.c (1)

273-281: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider wrapping the delete loop in a single transaction.

Each flb_tail_db_file_delete_by_id call runs in its own implicit transaction. With db.sync full and a database that holds many stale rows, startup performs one durable commit per deleted row. A single explicit transaction around the loop reduces startup latency and makes the cleanup atomic.

This is optional if the expected stale-row count is small.

🤖 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 `@plugins/in_tail/tail_db.c` around lines 273 - 281, Wrap the stale-file
deletion loop in the surrounding cleanup flow with one explicit database
transaction, using the transaction helpers already available in the tail DB
implementation. Commit once after all flb_tail_db_file_delete_by_id calls
succeed, roll back on any failure before cleanup, and preserve deleted_count and
existing error propagation.
plugins/in_tail/tail.c (1)

490-521: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Optional: the monitored-inode lookup is linear per database row.

flb_tail_db_cleanup calls this callback for each stale-candidate row, and each call walks every matching input and every tracked file. The total cost is rows x inputs x files. For a deployment with thousands of tracked files and a large database this adds measurable startup time. If that scale is expected, build a hash set of monitored inodes once in in_tail_pre_run and pass it as data.

🤖 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 `@plugins/in_tail/tail.c` around lines 490 - 521, Optimize tail database
cleanup by building a hash set of monitored inode values once during
in_tail_pre_run, then pass that set to flb_tail_db_cleanup and
tail_db_inode_is_monitored instead of repeatedly traversing matching inputs and
files for each database row. Update the callback to perform constant-time inode
membership checks and ensure the set is released after cleanup.
plugins/in_tail/tail_db.h (1)

45-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the stale-file cleanup callback contract.

flb_tail_db_cleanup now takes a callback that must return FLB_TRUE when inode is still monitored. Add a short contract comment above the callback typedef in plugins/in_tail/tail_db.h.

🤖 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 `@plugins/in_tail/tail_db.h` around lines 45 - 47, Add a concise contract
comment above the inode monitoring callback typedef in tail_db.h, documenting
that the callback must return FLB_TRUE when the provided inode is still
monitored. Leave the flb_tail_db_cleanup declaration unchanged.
🤖 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.

Inline comments:
In `@plugins/in_tail/tail.c`:
- Around line 512-517: The monitored-inode check in tail_db_inode_is_monitored
must include both tail_ctx->files_static and tail_ctx->files_event so promoted
files remain protected; update plugins/in_tail/tail.c lines 512-517 accordingly.
Also verify in plugins/in_tail/tail.c lines 524-543 that all tail inputs sharing
the database handler complete discovery before cb_pre_run, and move cleanup
later if necessary so every file list is populated before stale records are
removed.

---

Nitpick comments:
In `@plugins/in_tail/tail_db.c`:
- Around line 273-281: Wrap the stale-file deletion loop in the surrounding
cleanup flow with one explicit database transaction, using the transaction
helpers already available in the tail DB implementation. Commit once after all
flb_tail_db_file_delete_by_id calls succeed, roll back on any failure before
cleanup, and preserve deleted_count and existing error propagation.

In `@plugins/in_tail/tail_db.h`:
- Around line 45-47: Add a concise contract comment above the inode monitoring
callback typedef in tail_db.h, documenting that the callback must return
FLB_TRUE when the provided inode is still monitored. Leave the
flb_tail_db_cleanup declaration unchanged.

In `@plugins/in_tail/tail.c`:
- Around line 490-521: Optimize tail database cleanup by building a hash set of
monitored inode values once during in_tail_pre_run, then pass that set to
flb_tail_db_cleanup and tail_db_inode_is_monitored instead of repeatedly
traversing matching inputs and files for each database row. Update the callback
to perform constant-time inode membership checks and ensure the set is released
after cleanup.

In `@tests/runtime/in_tail.c`:
- Around line 3146-3162: Update the rename/restart test around
test_tail_ctx_destroy to capture the original file inode with stat() before
renaming, then call tail_db_contains_file after the restart to assert the
renamed path remains associated with that inode. Retain the existing
output-count assertions and unlink test_db_renamed.log.1 during final cleanup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53978600-14da-4f7a-acb4-1e1c6c03d753

📥 Commits

Reviewing files that changed from the base of the PR and between 3b22f9e and 04c2afb.

📒 Files selected for processing (4)
  • plugins/in_tail/tail.c
  • plugins/in_tail/tail_db.c
  • plugins/in_tail/tail_db.h
  • tests/runtime/in_tail.c

Comment thread plugins/in_tail/tail.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tail plugin incorrectly removing entries from the database file during startup

1 participant