Skip to content

[fix](logstash) Replace HttpClient5 async with HttpClient4 sync to fix CircularRedirectException (1.2.0 -> 1.2.1)#63181

Merged
JNSimba merged 5 commits into
apache:masterfrom
bingquanzhao:logstash-fix
Jun 8, 2026
Merged

[fix](logstash) Replace HttpClient5 async with HttpClient4 sync to fix CircularRedirectException (1.2.0 -> 1.2.1)#63181
JNSimba merged 5 commits into
apache:masterfrom
bingquanzhao:logstash-fix

Conversation

@bingquanzhao

Copy link
Copy Markdown
Contributor

Replace HttpClient5 async with HttpClient4 sync to fix CircularRedirectException (1.2.0 -> 1.2.1)

The logstash-output-doris plugin uses Apache HttpClient5 async client to PUT stream load requests. Against SelectDB Cloud / BYOC FE — which returns '307 + Connection: close' on stream load — the async client fails with CircularRedirectException under any meaningful concurrency / body size.

Root cause:

  1. HC5 async does not strictly block body transmission while waiting for '100 Continue'. When FE returns 307 before issuing 100, the entity producer has already started writing; FE closing the connection then yields an IOException mid-transfer.
  2. HC5 default exec chain wraps RedirectExec around AsyncHttpRequestRetryExec. The recoverable IOException triggers an internal retry that re-enters the same FE -> 307 path, but RedirectLocations from the first attempt is still populated, so the same BE URL is detected as 'already visited' and reported as a circular redirect.

This is a real HC5-vs-HC4 implementation difference, not a configuration issue. The Doris Flink connector also follows FE 307 to BE in its default path (autoRedirect=true) and works correctly precisely because it uses HC4 sync: HC4 honors 'Expect: 100-continue' strictly, so when FE 307s without sending 100, the entity is left unconsumed and HC4's RedirectExec follows the redirect normally.

This patch aligns the plugin with the Flink connector's HTTP layer:

  • bump gem version 1.2.0 -> 1.2.1
  • httpclient5 5.4.2 (async) -> httpclient 4.5.13 (sync)
  • SimpleRequestBuilder -> HttpPut + ByteArrayEntity (repeatable)
  • HttpAsyncClients defaults -> HttpClients with:
    • setRequestExecutor(HttpRequestExecutor(60s))
    • setRedirectStrategy(DorisRedirectStrategy) (isRedirectable=true,
      strip userinfo, normalize empty query)
    • setRetryHandler(DefaultHttpRequestRetryHandler(0, false))
    • setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE)
    • RequestConfig.setExpectContinueEnabled(true)
  • Async future plumbing in TableEvents replaced with sync
    response_code / response_body / response_error fields.
  • Stringify both key and value at request.addHeader call site: HC4's
    addHeader(String, String) is strict on types whereas HC5 had a
    permissive (String, Object) overload; user configs commonly carry
    Float / Integer values like 'max_filter_ratio => 1.0'.
  • Drop 's.requirements << jar ...' from gemspec: with JARs vendored under
    lib/, the maven lookup at install time is unnecessary and forced users
    to set JARS_SKIP=true for offline installs.

Pipeline configuration, retry queue, save_on_failure, group_commit, label generation, header handling - all unchanged.

Verified on a SelectDB BYOC cluster mirroring the reported production shape (16 workers x 10000 batch x 200,000 events):

  • Before: 100% requests fail with CircularRedirectException
  • After: 20/20 stream loads Status=Success, 200,000/200,000 rows ingested, 0 HTTP-layer errors.

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

…x CircularRedirectException (1.2.0 -> 1.2.1)

The logstash-output-doris plugin uses Apache HttpClient5 async client to PUT
stream load requests. Against SelectDB Cloud / BYOC FE — which returns
'307 + Connection: close' on stream load — the async client fails with
CircularRedirectException under any meaningful concurrency / body size.

Root cause:
  1. HC5 async does not strictly block body transmission while waiting for
     '100 Continue'. When FE returns 307 before issuing 100, the entity
     producer has already started writing; FE closing the connection then
     yields an IOException mid-transfer.
  2. HC5 default exec chain wraps RedirectExec around
     AsyncHttpRequestRetryExec. The recoverable IOException triggers an
     internal retry that re-enters the same FE -> 307 path, but
     RedirectLocations from the first attempt is still populated, so the
     same BE URL is detected as 'already visited' and reported as a
     circular redirect.

This is a real HC5-vs-HC4 implementation difference, not a configuration
issue. The Doris Flink connector also follows FE 307 to BE in its default
path (autoRedirect=true) and works correctly precisely because it uses
HC4 sync: HC4 honors 'Expect: 100-continue' strictly, so when FE 307s
without sending 100, the entity is left unconsumed and HC4's RedirectExec
follows the redirect normally.

This patch aligns the plugin with the Flink connector's HTTP layer:
  - bump gem version 1.2.0 -> 1.2.1
  - httpclient5 5.4.2 (async)  ->  httpclient 4.5.13 (sync)
  - SimpleRequestBuilder       ->  HttpPut + ByteArrayEntity (repeatable)
  - HttpAsyncClients defaults  ->  HttpClients with:
      * setRequestExecutor(HttpRequestExecutor(60s))
      * setRedirectStrategy(DorisRedirectStrategy) (isRedirectable=true,
        strip userinfo, normalize empty query)
      * setRetryHandler(DefaultHttpRequestRetryHandler(0, false))
      * setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE)
      * RequestConfig.setExpectContinueEnabled(true)
  - Async future plumbing in TableEvents replaced with sync
    response_code / response_body / response_error fields.
  - Stringify both key and value at request.addHeader call site: HC4's
    addHeader(String, String) is strict on types whereas HC5 had a
    permissive (String, Object) overload; user configs commonly carry
    Float / Integer values like 'max_filter_ratio => 1.0'.
  - Drop 's.requirements << jar ...' from gemspec: with JARs vendored under
    lib/, the maven lookup at install time is unnecessary and forced users
    to set JARS_SKIP=true for offline installs.

Pipeline configuration, retry queue, save_on_failure, group_commit,
label generation, header handling - all unchanged.

Verified on a SelectDB BYOC cluster mirroring the reported production
shape (16 workers x 10000 batch x 200,000 events):
  - Before: 100% requests fail with CircularRedirectException
  - After:  20/20 stream loads Status=Success, 200,000/200,000 rows
            ingested, 0 HTTP-layer errors.
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

xiaokang
xiaokang previously approved these changes May 12, 2026

@xiaokang xiaokang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label May 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@bingquanzhao

Copy link
Copy Markdown
Contributor Author

run buildall

JNSimba
JNSimba previously approved these changes May 25, 2026
@JNSimba

JNSimba commented May 25, 2026

Copy link
Copy Markdown
Member

run buildall

@bingquanzhao

Copy link
Copy Markdown
Contributor Author

run buildall

@JNSimba

JNSimba commented Jun 3, 2026

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found one blocking packaging regression. The documented build command succeeds, but the resulting gem no longer contains the generated jar loader or HttpClient jars while doris.rb still requires the loader unconditionally.

Critical checkpoint conclusions:

  • Goal: switching the Logstash plugin to HttpClient4-style stream-load handling is only partially achieved because the built plugin cannot load from the documented package output.
  • Scope: the runtime code change is focused, but the dependency metadata change removes the only visible source for the required jar loader/artifacts without adding an equivalent vendoring/build step.
  • Concurrency: the plugin remains concurrency :shared; the new CloseableHttpClient is designed for concurrent use. No new shared mutable per-request state issue found.
  • Lifecycle: no new static/global lifecycle issue found, but the plugin load lifecycle fails before registration when the required jar loader is absent.
  • Configuration: no new configuration items.
  • Compatibility: package/install compatibility regresses for users building/installing from this gemspec because required Java dependencies are no longer resolved or packaged.
  • Parallel code paths: only this Logstash extension path is changed.
  • Conditional checks: redirect customization is consistent with similar Doris stream-load clients.
  • Tests: no test or packaging validation was added; I verified locally that gem build logstash-output-doris.gemspec creates a gem whose lib/ contains only logstash/, not logstash-output-doris_jars.rb or vendored jars.
  • Observability: no new observability need for this change.
  • Transactions/data writes: stream-load semantics are involved, but I did not find a new transaction correctness issue in the reviewed runtime flow.
  • FE/BE variables: not applicable.
  • Performance: the new sync client serializes per-table requests that were previously submitted asynchronously, but I am not raising this as a blocker without a concrete workload regression requirement.

User focus: no additional user-provided review focus was supplied.

Comment thread extension/logstash/logstash-output-doris.gemspec Outdated
Reviewer pointed out that the documented `gem build logstash-output-doris.gemspec`
on a clean clone produces a gem whose `lib/` contains only `logstash/` -- no
`logstash-output-doris_jars.rb` loader and no vendored httpclient jars -- so
`require '.../logstash-output-doris_jars.rb'` in `doris.rb` fails at plugin
load time. The jar artifacts and loader are gitignored, so they must be
generated as part of the build.

Restore the dependency declaration so `Jars::Installer.new.vendor_jars` has
a manifest to resolve, and document the full flow in README:

  1. vendor_jars  -> downloads httpclient4 (+ transitive httpcore /
                     commons-logging / commons-codec) into lib/ and
                     generates the _jars.rb loader.
  2. gem build    -> packages those files into the 1.36 MB
                     logstash-output-doris-1.2.1-java.gem.
  3. install      -> `JARS_SKIP=true bin/logstash-plugin install <gem>`
                     (jars are already vendored, no Maven lookup needed).
  4. offline pack -> for air-gapped Logstash hosts, build an offline
                     pack with `bin/logstash-plugin prepare-offline-pack`
                     and install via `file://...zip` on the target.

Verified on Logstash 7.17.18 against local Doris docker (FE 8030 / BE 8040):
  - clean clone -> vendor_jars -> gem build -> install -> 5/5 stream loads
    Status=Success, 5/5 rows in Doris.
  - same flow via offline pack on a second Logstash install: 5/5 Success.
@bingquanzhao
bingquanzhao dismissed stale reviews from xiaokang and JNSimba via e0f0cc3 June 4, 2026 07:58
@bingquanzhao

Copy link
Copy Markdown
Contributor Author

run buildall

@JNSimba JNSimba left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@JNSimba

JNSimba commented Jun 4, 2026

Copy link
Copy Markdown
Member

run buildall

@bingquanzhao

Copy link
Copy Markdown
Contributor Author

/review

1 similar comment
@JNSimba

JNSimba commented Jun 8, 2026

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No new blocking issues found in this review.

Critical checkpoint conclusions:

  • Goal and proof: The PR targets the Logstash stream-load redirect failure by switching from HttpClient5 async to HttpClient4 sync with PUT redirects, disabled HTTP retries, no connection reuse, and Expect: 100-continue. The README now documents the vendoring/build/install flow. The commits describe manual Logstash/Doris verification, but I did not run those tests in this runner.
  • Scope: The change is focused on the Logstash extension code, gem dependency metadata, and README build/install documentation.
  • Concurrency: The plugin remains concurrency :shared; the shared CloseableHttpClient is the intended thread-safe HttpClient usage. The per-batch requests are now synchronous, which is a performance/behavior tradeoff from the old async submission, but I did not identify a correctness issue from it.
  • Lifecycle/static state: No new cross-TU/static initialization or non-obvious persisted lifecycle issue was found. Existing background thread/client lifecycle patterns are unchanged in kind.
  • Configuration: No Doris/Logstash configuration item was added.
  • Compatibility/storage format: No storage format, FE/BE protocol, or function symbol compatibility issue was introduced.
  • Parallel paths: Related Doris HttpClient stream-load code uses the same redirectable PUT pattern; no missing parallel Doris code path was found for this Logstash-only plugin fix.
  • Conditional checks: The new redirect and empty-query normalization checks have clear local rationale.
  • Tests/results: No new automated test files or generated outputs are included. Manual verification is described in the commit message; I did not independently execute JRuby/Logstash tests because JRuby is not available in this runner.
  • Observability: Existing request/response logging remains in place; no additional observability gap specific to this fix was found.
  • Transaction/data correctness: Stream-load success handling and retry/drop/save-on-failure flow are preserved; no new committed/uncommitted data visibility issue was found.
  • FE/BE variable passing: Not applicable.
  • Performance: The switch from async per-table submission to sync execution can reduce per-batch parallelism when one Logstash batch fans out to many target tables, but this appears inherent to the selected sync HttpClient4 approach and not a blocking correctness regression.

Existing review context: the earlier inline thread about missing vendored jar generation should not be duplicated; the current head restores s.requirements for HttpClient4 and documents vendor_jars before gem build.

User focus: no additional user-provided review focus was specified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants