shashank created CAMEL-25572:
--------------------------------

             Summary: camel-github2 - the consumers remember the items of a 
poll as consumed before routing them: commits of a poll with a failed author 
lookup are never routed, and a route suspended during a poll skips the rest of 
it
                 Key: CAMEL-25572
                 URL: https://issues.apache.org/jira/browse/CAMEL-25572
             Project: Camel
          Issue Type: Bug
          Components: camel-github
            Reporter: shashank
            Assignee: shashank


The five {{github2}} consumers (main 2a0a2b54f4ba) store their position for all 
the items of a poll before {{processBatch}} routes them:
* {{EventsConsumer.poll}}, line 65: {{lastEventId}} is set to the id of the 
newest event.
* {{PullRequestConsumer.poll}}, line 68: {{lastOpenPullRequest}} is set to the 
number of the newest pull request.
* {{PullRequestCommentConsumer.poll}}, line 83: {{lastCommentId}} is set while 
the exchanges are built.
* {{TagConsumer.poll}}, line 58: the tag name is added to {{tagNames}}.
* {{CommitConsumer.poll}}, lines 137-144: the hash is added to {{commitHashes}}.

Two things can end a poll before every item was routed, and in both cases the 
items that were not routed are behind the position, so the following polls skip 
them. That part is silent: in case 1 the failed poll itself is logged at WARN 
by {{ScheduledPollConsumer}} ("Failed polling endpoint ... Will try again at 
next poll"), but nothing says that the commits of that poll are never delivered 
afterwards.

1. {{CommitConsumer}} only, default options. The exchanges are built (lines 
148-165) after the hashes were stored. {{GHCommit.getAuthor()}} and 
{{getCommitter()}} (lines 151-155) do requests of their own. In github-api 
1.330 (the version in {{parent/pom.xml}}) {{getAuthor()}} calls {{populate()}}, 
a GET of {{commits/<sha>}} once for each listed commit, and then 
{{GitHub.getUser(login)}}, a GET of {{users/<login>}} unless the login is 
cached. When one of them fails, the poll ends with the {{IOException}}, before 
{{processBatch}}. The next poll finds all those commits in {{commitHashes}} and 
routes none of them. What makes them fail: a 404 for the commit or the user 
(for example a deleted account), a server error, or a network error after the 
two connection retries of the client. A rate limit does not, with the client 
Camel creates: {{GitHubClientFactory}} uses the {{GitHubBuilder}} defaults 
({{RateLimitHandler.WAIT}} / {{AbuseLimitHandler.WAIT}}), which sleep and 
retry. It does with a client from the registry that is configured to fail on a 
rate limit.

2. All five consumers (for {{EventsConsumer}}, {{PullRequestConsumer}}, 
{{PullRequestCommentConsumer}} and {{TagConsumer}} this is the only case): 
{{AbstractGitHub2Consumer.processBatch}} (line 90) and 
{{CommitConsumer.processBatch}} (line 184) stop when {{isBatchAllowed()}} 
returns false. That is the case while the route is being suspended 
({{suspendRoute}}, {{CamelContext.suspend()}}, a route policy): 
{{DefaultShutdownStrategy}} calls {{deferShutdown(CompleteCurrentTaskOnly)}} on 
the consumer, so the batch ends after the exchange that is being routed. After 
{{resumeRoute}} the same consumer continues behind the items it did not route. 
(A stop and start of the route is not this defect: {{doStart}} of 
{{CommitConsumer}}, lines 54-61, {{PullRequestConsumer}}, 43-49, 
{{PullRequestCommentConsumer}}, 45-54, and {{TagConsumer}}, 42-47, sets the 
position again.) The loops also stop at {{maxMessagesPerPoll}}, which is not an 
endpoint option of github2 and can only be set on the consumer in code.

h3. Reproduction

New {{GitHub2ConsumerPositionTest}} (8 tests): real routes, the {{GitHub}} 
client is a Mockito mock bound as {{github2Client}} (the lookup the consumers 
already do). On main all 8 fail:
* {{commitsOfAPollWithAFailedAuthorLookupAreNotLost}} (three new commits, 
{{getAuthor()}} of the second one throws an {{IOException}} once): {{the 
commits of the poll with the failed lookup ==> expected: <[c1, c2, c3]> but 
was: <[]>}}.
* {{aCommitWhoseAuthorLookupFailsOnceIsRoutedWithTheNameFromTheCommit}} and 
{{aCommitWhoseUserLookupsAlwaysFailIsRoutedOnceAndDoesNotFailThePolls}} (the 
lookups of the second commit always fail with a {{GHFileNotFoundException}}): 
{{the commit and the ones that follow it are routed ==> expected: <[c1, c2, 
c3]> but was: <[]>}}.
* {{eventsNotRoutedByASuspendingConsumerAreRoutedAfterTheResume}} (four new 
events, the route is suspended while the second one is routed, then resumed): 
{{routed after the resume ==> expected: <[1, 2, 3, 4]> but was: <[1, 2]>}}.
* The same for pull requests ({{<[1, 2, 3, 4]> but was: <[1, 2]>}}), pull 
request comments ({{<[11, 12, 13, 14]> but was: <[11, 12]>}}), tags ({{<[v1, 
v2, v3, v4]> but was: <[v1, v2]>}}) and commits ({{<[c1, c2, c3, c4]> but was: 
<[c1, c2]>}}).

h3. Proposed fix

{{AbstractGitHub2Consumer.processBatch}} calls a new protected method 
{{onRouting(Exchange)}} right before it hands an exchange to the route (no-op 
by default, so a custom subclass still compiles). The five consumers move their 
position there, one exchange at a time, and no longer in {{poll}}. 
{{CommitConsumer}} already set {{lastSha}} at that point in its own copy of 
{{processBatch}}; the copy is removed and {{lastSha}} and {{commitHashes}} are 
updated in {{onRouting}}.

With the position moved only at routing, a lookup that always fails (the 404 of 
a deleted account) would fail every poll and hold back the commits after that 
one. So a failed {{getAuthor()}} / {{getCommitter()}} no longer ends the poll: 
the commit is routed, {{CamelGitHubCommitAuthor}} / 
{{CamelGitHubCommitCommitter}} get the name that git recorded in the commit 
({{GHCommit.getCommitShortInfo()}}, part of the list, no request) instead of 
the name of the GitHub profile, or are not set when the commit has no name, and 
one WARN line without stack trace is logged for the commit. The sha and url 
headers and the body come from the list and are as before. The GET of the 
commit and the GET of the user are handled the same way, as the consumer needs 
nothing from the former.

Not changed: an exchange whose route fails is not routed again (as before); the 
exchanges that were built and not routed are dropped as before; the value 
{{processBatch}} returns.

4.24 upgrade-guide entry (the guide for 4.24 is not on main yet: the change 
adds the file and its line in the index). No new option. camel-github2 unit 
tests with the fix: 14, 0 failures (the integration tests need a GitHub token 
and were not run).

How it was found: a small TLA+ model of a poll gave a 4-state trace (one new 
item is read, building the exchange fails) and a 6-state trace (the route is 
suspended during the batch) in which the consumer is past an item it did not 
route. Both were then reproduced with the real consumers and real routes as 
described; the model does not cover the GitHub client.

Affected: main and camel-4.23.x (197e70656a41: the six consumer classes are 
identical to main, {{git diff}} empty); camel-4.22.x and camel-4.18.x have the 
same lines in {{EventsConsumer}} and {{CommitConsumer}} (read, not run). 
camel-github2 exists since 4.18. Only {{CommitConsumer}} loses items without a 
suspend (case 1). {{EventsConsumer}}, {{PullRequestConsumer}}, 
{{PullRequestCommentConsumer}} and {{TagConsumer}} are affected only when the 
route is suspended during a batch (case 2).

Duplicate check (2026-10-12): JIRA text "github2 consumer" (none), 
"CommitConsumer commitHashes" (1: CAMEL-18439, the old camel-github commit 
consumer failing with more than 100 commits), "PullRequestCommentConsumer" 
(none), "github2 maxMessagesPerPoll" (none); GitHub pull requests "github2 
consumer": #23454 and its backport (header constant names), nothing about the 
position; no open pull request touches camel-github2.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to