Skip to content

out_s3: fix retry_limit semantics and multipart upload memory leaks#11669

Open
singholt wants to merge 3 commits intofluent:masterfrom
singholt:fix/s3-retry-limit-semantics
Open

out_s3: fix retry_limit semantics and multipart upload memory leaks#11669
singholt wants to merge 3 commits intofluent:masterfrom
singholt:fix/s3-retry-limit-semantics

Conversation

@singholt
Copy link
Copy Markdown
Contributor

@singholt singholt commented Apr 3, 2026

Background

The S3 plugin has its own internal buffer and retry system separate from the engine. When the engine flushes a chunk, cb_s3_flush writes data to its own filesystem store and returns FLB_OK immediately. The actual S3 upload happens later via a periodic timer callback (cb_s3_upload), which tracks failures internally and checks against retry_limit. When retry attempts are exhausted, chunks are orphaned on disk — not cleanly dropped like engine-managed retries.

For multipart uploads, the cost of exhausting retries is especially high — if 8 out of 10 parts have been uploaded and the 9th fails, the entire upload is abandoned. The already-uploaded parts are wasted and sit on S3 until lifecycle rules clean them up.

Problem

The change in f4108db (#10825) replaced the hardcoded MAX_UPLOAD_ERRORS (5) with ctx->ins->retry_limit to honor user config. This introduced two issues:

  1. The plugin used >= to compare failures against retry_limit, while the engine uses > semantics where retry_limit=N means N retries after the initial attempt. With >=, retry_limit=1 resulted in 0 retries (1 total attempt).

  2. The engine default for retry_limit is 1, which is too low for S3's internal retry system. With default settings (60s timer ticks), chunks are abandoned after just 2 attempts ~60 seconds apart. For multipart uploads, this means
    a single transient S3 error can cause an entire in-progress upload to be abandoned along with all its already-uploaded parts. The original hardcoded default of 5 gave ~5 minutes of retry window, enough to ride out transient S3 errors.

Additionally, the multipart upload error paths had memory leaks:

  1. When completion errors exceed retry_limit, mk_list_del removes the upload from the list but multipart_upload_destroy is never called, leaking ~80KB per abandoned upload (the etags array). Over days of running, this causes OOM kills.

  2. When chunk failures exceed retry_limit, flb_fstore_file_inactive frees the fstore file but not the s3_file context attached to it, leaking 48 bytes per abandoned chunk.

  3. The JSON chunk buffer allocated in cb_s3_flush was not freed on several return paths.

Evidence

Stability tests running Fluent Bit v4.2.3 with S3 multipart uploads showed tasks being OOM-killed (exit code 137) after running for days. CloudWatch logs confirmed SIGSEGV in get_upload → strcmp on a dangling pointer, and core
dump analysis showed the crash at s3.c:1668 iterating ctx->uploads after an upload was removed without being destroyed.

Valgrind analysis of the S3 tests confirmed:

  • Before fix: 80,128 bytes definitely lost (80,080 byte multipart_upload struct + 48 byte s3_file)
  • After fix: 0 bytes definitely lost across all 15 tests

Changes

  • Add retry_limit_is_set flag to flb_output_instance so plugins can distinguish between the engine default and explicit user configuration
  • Change all six >= comparisons to > so retry_limit=N correctly allows N retries (N+1 total attempts)
  • Use retry_limit_is_set to default to MAX_UPLOAD_ERRORS (5) when the user has not explicitly set retry_limit. If the user set a value, honor it. Log a warning when capping an explicit unlimited setting
  • Add multipart_upload_destroy after mk_list_del when completion errors are exhausted
  • Free s3_file before flb_fstore_file_inactive when chunk failures are exhausted
  • Fix the mock if/else if chain so CreateMultipartUpload returns a valid response (multipart mocks were previously non-functional)
  • Skip size validation in test mode so multipart paths can be exercised with small data
  • Remove the unit_test_flush bypass so tests exercise the real flush path
  • Add a mock call counter for test observability
  • Skip the 6s timer floor in test mode for faster test execution

Testing

All tests use upload_timeout=6s with sleep(10) and unique store_dir via mkdtemp for isolation. All 15 tests pass under valgrind with 0 bytes definitely lost.

Retry limit tests:

  • putobject_retry_limit_semantics: verifies retry_limit=1 results in exactly 2 PutObject attempts
  • default_retry_limit: verifies default retry_limit (5) results in 6 PutObject attempts

Multipart tests now exercise the real multipart upload path and assert call counts:

  • multipart_success: asserts CompleteMultipartUpload == 1
  • create_upload_error: asserts CreateMultipartUpload >= 1, UploadPart == 0
  • upload_part_error: asserts UploadPart >= 1, CompleteMultipartUpload == 0
  • complete_upload_error: asserts CompleteMultipartUpload >= 2 (retried)

PutObject, compression, and preserve_data_ordering tests assert the correct API path is used.

Documentation

Backporting

  • N/A - will need to be backported later if deemed necessary.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Detects whether an output instance's retry_limit was explicitly set, changes retry-limit checks from >= to > across S3 upload failure paths, adjusts defaulting and timer-clamping behavior under test, adds a test-only helper to increment mock S3 call counters via env vars, removes a unit-test-only flush helper, and adds runtime tests for PutObject retry semantics.

Changes

Cohort / File(s) Summary
S3 Plugin Core
plugins/out_s3/s3.c
Added test-only helper mock_s3_call_increment_counter(api) and call at mock entry; changed retry-limit comparisons from >= to > across failure paths; defaulted ctx->ins->retry_limit when retry_limit_is_set is false; made timer wait clamping conditional (skip when under test); removed unit_test_flush() special-case; adjusted file-discard logic to >.
Output instance state
include/fluent-bit/flb_output.h, src/flb_output.c
Added int retry_limit_is_set to struct flb_output_instance; initialize to false in flb_output_new() and set to true in flb_output_set_property() when retry_limit is provided (handles special string values and numeric parsing).
S3 Runtime Tests
tests/runtime/out_s3.c
Added upload_timeout="6s" to many S3 tests; ensure FLB_S3_PLUGIN_UNDER_TEST is unset in teardowns; added tests flb_test_s3_putobject_retry_limit_semantics and flb_test_s3_default_retry_limit to validate PutObject retry-count behavior and default retry_limit semantics; registered new tests in TEST_LIST.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • cosmo0920
  • PettitWesley

Poem

🐰 I hop through S3 and count each try,
Env vars whisper numbers as calls fly.
Flags tell me if limits were ever known,
Timers hush when tests call their own.
I nibble failures and watch uploads carry on 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main objective of the PR: fixing retry_limit semantics and handling multipart upload memory leaks in the S3 output plugin.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ 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 and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

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: 244de09d77

ℹ️ 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".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
plugins/out_s3/s3.c (1)

3841-3847: ⚠️ Potential issue | 🟠 Major

The queued flush path still has the old cutoff.

This discard check was updated, but when cb_s3_flush() sends a ready file through s3_upload_queue(), the queue still drops on upload_contents->retry_counter >= ctx->ins->retry_limit at Line 1946. With preserve_data_ordering=true by default, that path still allows only N total attempts instead of N + 1.

Suggested follow-up
-            if (upload_contents->retry_counter >= ctx->ins->retry_limit) {
+            if (upload_contents->retry_counter > ctx->ins->retry_limit) {
As per coding guidelines, "Before patching: trace one full path for affected signals (input -> chunk -> task -> output -> engine completion)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/out_s3/s3.c` around lines 3841 - 3847, The queued flush path
(cb_s3_flush -> s3_upload_queue) still drops uploads when
upload_contents->retry_counter >= ctx->ins->retry_limit which enforces only N
attempts; change that check to match the discard logic used elsewhere (use >
ctx->ins->retry_limit rather than >=) so a file gets retry_limit + 1 total
attempts (honoring preserve_data_ordering behavior). Update the conditional in
s3_upload_queue that checks upload_contents->retry_counter against
ctx->ins->retry_limit to use the same comparison semantics as the upload_file
discard path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/out_s3/s3.c`:
- Around line 654-658: The fallback to MAX_UPLOAD_ERRORS should only run when
the user did not set retry_limit; change the condition around
flb_output_get_property("retry_limit", ins) so you do not overwrite an
explicitly parsed ctx->ins->retry_limit (including negative
FLB_OUT_RETRY_UNLIMITED). In practice, remove the "|| ctx->ins->retry_limit < 0"
check and leave the assignment to ctx->ins->retry_limit = MAX_UPLOAD_ERRORS only
when flb_output_get_property("retry_limit", ins) == NULL so the parsed value
from ctx->ins->retry_limit is honored.

In `@tests/runtime/out_s3.c`:
- Line 496: The test uses a fixed store_dir in the flb_output_set call (out_ffd)
which causes cross-run state leakage and flaky PutObject counts; change the test
setup in tests/runtime/out_s3.c to create a unique temporary directory for
store_dir (or explicitly remove/clean the directory before and after each test)
and pass that path to flb_output_set for the S3 output instance (the same code
that sets "store_dir" for out_ffd and any other out_* instances near those
calls); ensure cleanup runs even on failures so TEST_PutObject_CALL_COUNT is
only influenced by the current test run.

---

Outside diff comments:
In `@plugins/out_s3/s3.c`:
- Around line 3841-3847: The queued flush path (cb_s3_flush -> s3_upload_queue)
still drops uploads when upload_contents->retry_counter >= ctx->ins->retry_limit
which enforces only N attempts; change that check to match the discard logic
used elsewhere (use > ctx->ins->retry_limit rather than >=) so a file gets
retry_limit + 1 total attempts (honoring preserve_data_ordering behavior).
Update the conditional in s3_upload_queue that checks
upload_contents->retry_counter against ctx->ins->retry_limit to use the same
comparison semantics as the upload_file discard path.
🪄 Autofix (Beta)

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

Run ID: 577d876d-8a83-4f7b-9298-854d9c921c0d

📥 Commits

Reviewing files that changed from the base of the PR and between 3e414ac and 244de09.

📒 Files selected for processing (2)
  • plugins/out_s3/s3.c
  • tests/runtime/out_s3.c

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
plugins/out_s3/s3.c (1)

3351-3356: ⚠️ Potential issue | 🟠 Major

preserve_data_ordering still keeps the old retry boundary.

These > changes only fix the timer-based path. s3_upload_queue() still uses upload_contents->retry_counter >= ctx->ins->retry_limit on Line 1950, so queued uploads in the default preserve_data_ordering=true mode still allow only N total attempts instead of N+1. Change that check to > too so both internal retry paths agree.

Suggested fix
-            if (upload_contents->retry_counter >= ctx->ins->retry_limit) {
+            if (upload_contents->retry_counter > ctx->ins->retry_limit) {
                 flb_plg_warn(ctx->ins, "Chunk file failed to send %d times, will not "
                              "retry", upload_contents->retry_counter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/out_s3/s3.c` around lines 3351 - 3356, s3_upload_queue() currently
uses a >= comparison (upload_contents->retry_counter >= ctx->ins->retry_limit)
which keeps the old retry boundary; change that check to use > so it matches the
timer-based path (as used where chunk->failures > ctx->ins->retry_limit) —
update the condition in s3_upload_queue to compare
upload_contents->retry_counter > ctx->ins->retry_limit so both retry paths allow
N+1 attempts and remain consistent.
♻️ Duplicate comments (1)
plugins/out_s3/s3.c (1)

654-661: ⚠️ Potential issue | 🟠 Major

Only fall back when Retry_Limit was omitted.

retry_limit_is_set already distinguishes the unset case. The extra ctx->ins->retry_limit < 0 branch rewrites an explicit Retry_Limit no_limits/off/false to MAX_UPLOAD_ERRORS, so the plugin no longer honors the parsed value.

Suggested fix
-    if (ins->retry_limit_is_set == FLB_FALSE || ctx->ins->retry_limit < 0) {
+    if (ins->retry_limit_is_set == FLB_FALSE) {
         ctx->ins->retry_limit = MAX_UPLOAD_ERRORS;
     }

Based on learnings, "In the Fluent Bit S3 plugin, the user prefers to maintain current retry_limit behavior without special handling for FLB_OUT_RETRY_UNLIMITED (-1), as there's no documentation indicating -1 should be used for infinite retries and consistency with current logic is preferred."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/out_s3/s3.c` around lines 654 - 661, The code currently overrides an
explicitly set negative retry value because the condition checks both
retry_limit_is_set and ctx->ins->retry_limit < 0; change the logic to only apply
the MAX_UPLOAD_ERRORS default when the user omitted Retry_Limit by removing the
ctx->ins->retry_limit < 0 check so that retry_limit_is_set (and any explicit
negative value such as FLB_OUT_RETRY_UNLIMITED) is honored; update the block
that assigns ctx->ins->retry_limit = MAX_UPLOAD_ERRORS to run only when
ins->retry_limit_is_set == FLB_FALSE, referencing retry_limit_is_set and
ctx->ins->retry_limit and keeping MAX_UPLOAD_ERRORS as the fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugins/out_s3/s3.c`:
- Around line 3351-3356: s3_upload_queue() currently uses a >= comparison
(upload_contents->retry_counter >= ctx->ins->retry_limit) which keeps the old
retry boundary; change that check to use > so it matches the timer-based path
(as used where chunk->failures > ctx->ins->retry_limit) — update the condition
in s3_upload_queue to compare upload_contents->retry_counter >
ctx->ins->retry_limit so both retry paths allow N+1 attempts and remain
consistent.

---

Duplicate comments:
In `@plugins/out_s3/s3.c`:
- Around line 654-661: The code currently overrides an explicitly set negative
retry value because the condition checks both retry_limit_is_set and
ctx->ins->retry_limit < 0; change the logic to only apply the MAX_UPLOAD_ERRORS
default when the user omitted Retry_Limit by removing the ctx->ins->retry_limit
< 0 check so that retry_limit_is_set (and any explicit negative value such as
FLB_OUT_RETRY_UNLIMITED) is honored; update the block that assigns
ctx->ins->retry_limit = MAX_UPLOAD_ERRORS to run only when
ins->retry_limit_is_set == FLB_FALSE, referencing retry_limit_is_set and
ctx->ins->retry_limit and keeping MAX_UPLOAD_ERRORS as the fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3e72dbc-35c0-44a5-adc6-cf6040f332b6

📥 Commits

Reviewing files that changed from the base of the PR and between 244de09 and ed39078.

📒 Files selected for processing (4)
  • include/fluent-bit/flb_output.h
  • plugins/out_s3/s3.c
  • src/flb_output.c
  • tests/runtime/out_s3.c

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/runtime/out_s3.c`:
- Line 479: The calls to mkdtemp(store_dir) are unchecked (the return value is
ignored), so if mkdtemp fails the test will proceed with an invalid store_dir;
update both occurrences (the mkdtemp calls that use the store_dir variable) to
capture the return value, check for NULL, and handle the failure by calling a
test-fatal/abort path (e.g., fprintf(stderr, ...) and exit/fail the test) or
using the existing test framework's failure macro so the test stops immediately
with a clear error message.
🪄 Autofix (Beta)

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

Run ID: 0f06ff18-ab17-42ba-a265-aabff213f4b5

📥 Commits

Reviewing files that changed from the base of the PR and between ed39078 and 90258d7.

📒 Files selected for processing (1)
  • tests/runtime/out_s3.c

@singholt singholt changed the title out_s3: fix retry_limit semantics and default out_s3: fix retry_limit semantics and multipart upload memory leaks Apr 3, 2026
singholt and others added 3 commits April 3, 2026 17:05
Add a boolean to flb_output_instance that tracks whether
retry_limit was explicitly set by the user. This allows
plugins to distinguish between the engine default and an
explicit user configuration.

Co-authored-by: Thean Lim <theanlim@amazon.com>
Signed-off-by: Anuj Singh <singholt@amazon.com>
Fix retry_limit off-by-one: change all six >= comparisons to >
so retry_limit=N correctly allows N retries (N+1 total attempts),
matching engine semantics.

Default retry_limit to MAX_UPLOAD_ERRORS (5) when the user has
not explicitly set it, using the new retry_limit_is_set flag.
The engine default of 1 is too low for S3's internal retry
system where partially uploaded multipart data is wasted when
retries are exhausted too early. Log a warning when capping an
explicit unlimited setting to MAX_UPLOAD_ERRORS.

Fix multipart upload memory leaks:
- Add multipart_upload_destroy after mk_list_del when completion
  errors exceed retry_limit (80KB per abandoned upload)
- Free s3_file before flb_fstore_file_inactive when chunk
  failures exceed retry_limit (48 bytes per abandoned chunk)
- Free the json chunk buffer in all cb_s3_flush return paths

Fix mock_s3_call if/else chain so CreateMultipartUpload returns
a valid XML response with UploadId. Skip size validation in
test mode so multipart paths can be exercised with small data.
Remove unit_test_flush bypass so tests exercise the real flush
path. Add mock call counter and skip the timer floor in test
mode for faster execution.

Co-authored-by: Thean Lim <theanlim@amazon.com>
Signed-off-by: Anuj Singh <singholt@amazon.com>
Add retry_limit tests:
- putobject_retry_limit_semantics: verify retry_limit=1
  results in exactly 2 PutObject attempts
- default_retry_limit: verify default retry_limit (5)
  results in 6 PutObject attempts

Add assertions to all existing tests to verify the correct
S3 API path is exercised (PutObject vs multipart) and the
expected number of API calls are made.

Add preserve_data_ordering test to exercise the upload
queue path.

Use upload_timeout=6s with sleep(10) consistently so the
timer fires and exercises the real upload path. Add unique
store_dir via mkdtemp to multipart tests. Clean up all API
call counters at the end of each test.

All 15 tests pass under valgrind with 0 bytes definitely
lost.

Co-authored-by: Thean Lim <theanlim@amazon.com>
Signed-off-by: Anuj Singh <singholt@amazon.com>
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.

2 participants