Buffer QuestDB writes through an outage and replay them - #18
Merged
Conversation
Reconnecting stopped the permanent wedge but still lost every reading taken while QuestDB was down. The sink now holds rows in memory and replays them in order once the connection is back. Rows are held until a flush confirms them, not just while the client knows it is disconnected. The ILP client keeps written rows in its own buffer until the next flush and throws that buffer away when the flush fails, so by the time the sink learns the socket is dead the readings it accepted are already gone. Holding them until the flush succeeds is what makes them recoverable. QuestDB.MaxBufferBytes caps the buffer, 4 MiB by default, sized from an estimate of the ILP encoding of each row. Past the cap the oldest rows are evicted and the store call starts failing, which the service escalates to a process exit. The pod then crash-loops until QuestDB is back, instead of growing until the kernel kills it. Zero restores the previous drop-on-disconnect behaviour. That needed a health-server change. Liveness would have restarted the pod after 90s and thrown away the buffer holding the data. A checker can now implement Degrader to say it is failing but recovering in place, and the liveness threshold skips it. Readiness still goes red immediately, so the outage stays visible. One limit stays: ILP over TCP has no server acknowledgement, so the last flush before a socket error reports success even though QuestDB never stored those rows. They cannot be replayed. Everything from the first reported failure onwards is covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #17. Reconnecting stopped the permanent wedge, but every reading taken while QuestDB was down was still dropped. This holds them and replays them.
Where the data was actually going
The obvious design (buffer rows once we know we are disconnected) does not catch the loss. Rows handed to the ILP client sit in its internal buffer until the next flush, and
tcpLineSender.Flushdiscards that buffer when the write fails. The sink only discovers the outage at flush time, by which point the rows that flush was carrying are gone.So the buffer holds every row until a flush confirms it, and clears on a successful flush. In the normal case that is one flush interval of rows.
Overflow
QuestDB.MaxBufferBytescaps it, 4 MiB by default. Sizing comes from asizingSenderthat implementsLineSenderand measures the ILP encoding of a row; the client does not expose the encoded length. Estimates round up, so the cap is not exceeded.Past the cap the oldest rows are evicted and the store call starts returning errors, which the service's consecutive-error escalation turns into a non-zero exit after five. The pod crash-loops for as long as QuestDB is down rather than growing until the kernel kills it.
MaxBufferBytes: 0restores the v1.5.3 drop-on-disconnect behaviour.Health: degraded is not stuck
This needed a health-server change, otherwise the fix defeats itself: liveness flips after 90s of unhealthiness and the restart throws away the buffer that is holding the data.
healthserver.Degraderis an optionalCheckerextension. A checker that is failing but recovering in place reportsDegraded() trueand the liveness threshold skips it. Readiness is untouched and goes red on the first failure, so the outage is still visible and alertable./readyz/healthzKnown limit
ILP over TCP has no server acknowledgement. The last flush before a socket error is reported as successful even though QuestDB never stored those rows, so they cannot be replayed. The buffer covers everything from the first reported failure onwards. Closing that gap properly means moving to the HTTP transport, which gets a real server response; that is a bigger change and a separate decision.
Tests