shashank created CAMEL-26107:
--------------------------------

             Summary: camel-wal - WriteAheadResumeStrategy never replays the 
unconfirmed offsets after a restart: start() truncates the log before 
loadCache() reads it
                 Key: CAMEL-26107
                 URL: https://issues.apache.org/jira/browse/CAMEL-26107
             Project: Camel
          Issue Type: Bug
            Reporter: shashank
            Assignee: shashank


{{WriteAheadResumeStrategy}} keeps the offsets that the wrapped resume strategy 
has not confirmed yet in a log file, and is meant to replay them (the entries 
in state {{NEW}} or {{FAILED}}) on the next start. After a restart nothing is 
replayed (main 7b58ce0cdaf6):

* {{start()}} creates {{new LogWriter(logFile, flushPolicy)}}; the 
{{LogWriter}} constructor opens the file with {{CREATE, WRITE, 
TRUNCATE_EXISTING}} and writes a new header.
* {{loadCache()}} reads the file with a {{LogReader}} and replays the 
{{NEW}}/{{FAILED}} entries.
* Camel calls {{start()}} first: {{ResumableProcessor.doStart}} starts the 
strategy, and the consumers (cassandraql, couchbase, couchdb, mongodb change 
streams, through {{ResumeStrategyHelper.resume}}; file and aws2-kinesis call 
{{loadCache()}} directly) call {{loadCache()}} in their own {{doStart}}. The 
{{Splitter}} also starts the strategy before {{loadCache()}}.

So {{loadCache()}} reads a file with only the 12-byte header. Checked with a 
test: three {{NEW}} records, 135 bytes before {{start()}}, 12 bytes after, 
nothing replayed; with the bytes written back after {{start()}}, all three are 
replayed. The offsets that were in flight when the JVM stopped are lost 
silently, which is what the component is for.

Once the log is read, two {{LogReader}} cases become reachable: a record cut 
short at the end of the file (a crash while writing it) throws 
{{BufferUnderflowException}} when the record spans two reads of the reader's 
512 KiB buffer, and a negative slot length throws 
{{NegativeArraySizeException}}; a file shorter than the header throws 
{{BufferUnderflowException}} from the constructor.

*Fix*: {{start()}} reads the unconfirmed entries of the existing log before it 
creates the writer, and appends them to the new log, so they survive another 
crash before the replay. As the wrapped strategy keeps the last offset of each 
key, only the last entry of each key in the log counts: it is replayed if it is 
{{NEW}} or {{FAILED}}, and an earlier entry of the same key is not (an old 
{{FAILED}} offset must not replace a later offset of the key that was 
confirmed, and the entries of a wrapped strategy that never calls back, such as 
the mongodb one, must not pile up across restarts). {{loadCache()}} replays 
them through the same path as a new offset, so the callback of the wrapped 
strategy marks each one {{PROCESSED}} or {{FAILED}} at its position in the new 
log (the replay no longer writes through {{updateState(PersistedLogEntry, 
...)}}). A replay that fails, or an entry the adapter cannot deserialize, is 
logged with a WARN and does not stop the other entries nor the start of the 
route; the entry stays in the log. {{LogReader}} throws 
{{InvalidRecordException}} for an incomplete record or header (also across two 
buffer reads) and for a negative slot length; {{start()}} logs a WARN and 
replays the entries before such a record, and a file of another format is 
replaced by a new log with a WARN, as before. The route start does not fail on 
a damaged log.

*Tests*: new {{WriteAheadResumeStrategyRestartTest}}, 23 tests through 
{{start()}} + {{loadCache()}}: NEW and FAILED replayed, PROCESSED not; only the 
last offset of each key; no pile-up with a wrapped strategy that never 
confirms; an entry that cannot be replayed does not stop the others nor the 
start; a failed replay is replayed on the next start; entries stay in the log 
until replayed; a last record truncated at 5 places, and across the reader 
buffer; a negative, huge or too long slot length and an invalid state; no file, 
empty, header only, shorter than a header, other format, other version. 17 fail 
on main ({{expected: <[key-0=0, key-2=2, key-3=3]> but was: <[]>}}), the 6 
'nothing to replay' cases pass on main. With the strategy change but the old 
{{LogReader}}, 4 fail with {{NegativeArraySizeException}} / 
{{BufferUnderflowException}}. camel-wal: 46 tests, 0 failures; merged with the 
CAMEL-26088 branch: 50, 0 failures.

*Upgrade guide*: 4.24 entry, as users now get the replay after a restart (the 
documented intent).

*Not changed*: after a roll-over the log holds the newer entries before the 
older ones, so the last entry of a key and the replay order are those of the 
file, not the order of the offsets (no layer information in the file). 
{{LogReader}} still gives a {{PersistedLogEntry}} the channel position after 
the buffer fill; no caller writes at it any more.

*Affected*: main, camel-4.23.x, camel-4.22.x, camel-4.18.x and camel-4.14.x 
(same camel-wal Java sources, and {{start()}} before {{loadCache()}} in 
{{ResumableProcessor}}/{{ResumeStrategyHelper}}). Read, not run on the branches.

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to