Drain consumer queues at shutdown and make the lookup pause claim atomic - #1014
Open
tricrotism wants to merge 5 commits into
Open
tricrotism wants to merge 5 commits into
tricrotism wants to merge 5 commits into
Conversation
On shutdown the consumer made one more pass over one buffer and did nothing if a lookup held the pause, so rows queued before a stop could be lost. It now drains both buffers until empty, up to 30 seconds, and runs past a held pause in the last 5. Lookups also claimed the pause with a check-then-set, so two could hold it at once and the first to finish released it under the other. The claim is now atomic.
❌ Deploy Preview for coreprotect failed. Why did it fail? →
|
Contributor
|
Thanks -- automated review is requesting the following changes:
Also, the description should say six lookup pause-acquisition sites are updated, rather than four. |
Lookups and the consumer now claim and release the pause through Consumer, which records who holds it, so neither side can clear the other's claim, including when the consumer writes past a lookup in the last 5 seconds of a shutdown. The consumer claims the pause after taking the lifecycle lock instead of acting on an earlier read of the flag. The 30-second drain deadline now also bounds the consumer delay, the reservation wait and the lifecycle lock wait; a pass that is already writing is not interrupted. If rows are still queued when the consumer exits, ShutdownService reports how many were discarded instead of the normal disable message.
…down-final-drain # Conflicts: # src/main/java/net/coreprotect/database/LookupRaw.java
Contributor
|
The following changes are requested:
|
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.
Summary
Rows queued shortly before a server stop can be lost: the consumer makes exactly one more pass over one of its two buffers, and that pass does nothing if a lookup holds the consumer pause. Separately, two lookups can both claim the pause, and the first to finish releases it while the other is still reading. This change drains both buffers until they are empty (bounded at 30 seconds) and makes the pause claim atomic.
The problem
Shutdown drops queued rows
Consumer.run(consumer/Consumer.java:485) loops whileserverRunning. When the server stops it setslastRunand goes round once more, then exits. That last pass:consumer_id0 and 1), so rows sitting in the other one, from a pass that deferred or from producers that were still queueing when the buffer switched, are never written;Consumer.java:467ifisPausedis set, which is the case whenever a lookup, rollback or API lookup is running at stop time. The final pass then writes nothing at all;Database.getConnection(database/Database.java:420) for that same pause to clear, then getnulland write nothing;errorDelay()before the thread exits, holding up the stop.Scenario: a staff member runs
/co lookupor a plugin polls the API while the server restarts. Every block, container and chat row queued in the last seconds before the stop is gone.Lookups can overlap
Every lookup claims the pause like this (
database/Lookup.java:62-65,database/LookupRaw.java:88-91and169-173):Two lookup threads can both see
falseand both settrue. When the first one finishes it setsisPaused = false, and the consumer starts a write pass while the second lookup is still reading. On SQLite that is the lock contention the pause exists to prevent.The fix
consumer/Consumer.javaconsumer_id[i][1]), up to a 30-second deadline.errorDelay().claimLookupPause(): waits for the flag to clear and sets it inside one monitor, so two lookups cannot both claim it.consumer/process/Process.java,database/Database.javaDatabase.getConsumerConnection(waitTime, ignorePause). On the shutdown passes it skips the SQLite wait onisPaused, sinceConsumer.runhas already decided whether the pause blocks the pass. Every other caller ofgetConnectionis unchanged.database/Lookup.java,database/LookupRaw.javaConsumer.claimLookupPause().Behaviour change
Risk
Processsets and clearsisPausedaround its own pass, so that lookup's claim can be cleared early. This only happens during the last 5 seconds of a stop, and the alternative is losing the queued rows.claimLookupPausepolls withwait(1), the same 1 ms granularity as the oldThread.sleep(1)loop. Nothing notifies it, so there is no missed-wakeup case to reason about.consumer_idstate through the existing synchronized map, and only afterserverRunningis false.Testing
Build:
mvn packagepasses.Row parity: the 47-step scenario, which ends by stopping the server, on Paper 26.2 and Folia 1.21.11 with SQLite. Folia matched upstream row for row. Paper differed only by the random plant that bone meal grows. No new errors. The scenario does not hold a lookup open across the stop, so it shows no regression rather than the lost-rows case.
Suggested test: on SQLite, have a plugin queue a few thousand block changes and start a long
/co lookupin the same tick, then/stop. Count the rows in the database after restart. Upstream loses the queued rows. This branch writes them, and the stop finishes within the 30-second window.