Transaction recovery takes a transaction whose final status it reads one poll later for a failed one
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- java
- Domain
- backend, databases, distributed-systems
Research direction
Start with StandardTransactionLogProcessor and KCVSLog to understand how recovery tracks processed log progress and expires transactions. Run the named JanusGraphIndexTest replay and recovery tests, plus the JanusGraph user-log recovery test, with their recovery-graph read options. Done means late final statuses are not double-counted, repairs are counted after being attempted, and the replay tests pass without fixed sleeps.
Written by the indexing model from the issue text.
Description
Bug
Transaction recovery (StandardTransactionLogProcessor) gives up on a transaction tx.max-commit-time after it has read one of the transaction's log entries (5 s at least). But it reads the transaction log in polls:
- one every
log.tx.read-interval(5 s by default); - each covering the log up to
log.tx.read-lag-timeback; - none going past the end of the 100 s timeslice it is in.
So recovery can read a transaction's final status up to two read intervals after its first entry, even when the commit took far less than tx.max-commit-time. The read lag cancels out: it delays every entry equally.
When the final status arrives after the expiry, recovery takes a transaction that succeeded for a failed one:
- It treats the transaction as failed at
PRIMARY_SUCCESS, restores the index documents of every element the transaction changed, and sends its user-log event again. ALogProcessorFrameworkprocessor then receives the transaction twice. The recovery docs (#4965) describe this as a risk only for commits that outlasttx.max-commit-time, but it also happens to commits that finished well inside it. - The late final status creates a second, empty entry for the same transaction. That entry expires as another transaction ("Trying to repair expired or unpersisted transaction"), so the transaction is counted twice.
How likely this is depends on how close tx.max-commit-time is to the read interval. With both at 5 s, the least recovery waits, a final status read one poll after the first entry arrives within milliseconds of the expiry, either before or after it.
Reproduction
A new JanusGraphIndexTest.testRecoveryWaitsForAFinalStatusReadOnePollLater:
- Settings:
tx.max-commit-time5 s,log.tx.read-interval6 s,log.tx.read-lag-time2 s. - The transaction writes a user log, and its index write takes 3 s and then succeeds.
- Recovery is started as the commit returns. Its first poll takes in the commit's first entries but not the final status, which was written less than 2 s earlier. The next poll comes 6 s later, after the entry has expired.
On master it fails 3 times out of 3 with expected: <1> but was: <2> user-log events.
The flaky replay tests
This is also behind the flaky JanusGraphIndexTest replay tests (testIndexReplay, testRecurringIndexReplay, testRecurringIndexReplayWithDifferentStartTime). They are BRITTLE, so of the CI jobs only the Solr jobs run them. They have three problems:
- Default read options on the recovery graph. They start recovery on a graph opened by a plain
clopen(). That drops thelog.tx.read-intervalof 250 ms andlog.tx.read-lag-timeof 50 ms they set earlier, because both options areMASKABLE. Recovery therefore polls every 5 s against a 5 s expiry (tx.max-commit-timeis 1 s, raised to the 5 s minimum). Locally the last transaction's final status is written less than 500 ms before the first poll, so only the next poll reads it, within milliseconds of the expiry.- Locally,
BerkeleyLuceneTest.testIndexReplayfailed withexpected: <4> but was: <5>. - With verbose recovery, runs show that transaction being repaired twice, the second time as "expired or unpersisted".
- Locally,
- A fixed 12 s wait. When a 100 s timeslice boundary falls between the schema transaction and the others, the others are read one poll later. They then expire just after the cleaner's second pass and are only counted at shutdown, after the test has read the statistics. This produced
CQLSolrTest.testRecurringIndexReplay:2585 expected: <4> but was: <0>in run 36059281715, where the success count of 1 had already been reached. - Counting before the repair.
getStatistics()counts a failed transaction before its repair starts. The repair runs on Caffeine's removal-listener executor, so a caller that sees the count cannot tell whether the repair has finished.
Fix
- Recovery gives a transaction up by how far it has read the log rather than after a wall-clock wait: once the log has been read, and its messages processed, up to
tx.max-commit-timepast the transaction's start. A wall-clock allowance can't cover it: polls start a read interval after the previous one ended, reads are retried for up tolog.tx.max-read-time, and all partitions share the read threads.KCVSLognow tracks how far its readers have processed what it read, and the recovery's cache uses that as its clock. - A failed transaction is counted once its repair has been attempted.
- The replay tests pass the fast read options to the graph they start recovery on, and wait for the statistics instead of sleeping a fixed 12 s, then long enough for a double count to show. The user-log recovery test in
JanusGraphTestgets the same options on its recovery graph.
A commit that really does outlast tx.max-commit-time, or whose instance fails between its user-log write and its final status, still gets its user-log event sent again. That is at-least-once delivery, and recovery cannot tell such a case from a failed user-log write. The repeat carries the transaction id of the original.
- Dominant language
- Java
- Stars
- 5.8k
- Forks
- 1.2k
- Avg merge
- 20h 36m
- Merged PRs (30d)
- 25
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from JanusGraph/janusgraph
-
Difficulty 1/5 Under an hour Newbie friendliness 95/100
JanusGraph/janusgraph#4943 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 62/100
JanusGraph/janusgraph#1578 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
JanusGraph/janusgraph#4954 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
JanusGraph/janusgraph#4934 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
JanusGraph/janusgraph#4923 · 1 comment ·
Maintainers usually reply within 1 day
All issues in JanusGraph/janusgraph
Similar issues
-
link-check link-check:manual
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 1/5 Under an hour Newbie friendliness 91/100
open-telemetry/opentelemetry-java#8870 ·
Maintainers usually reply within 1 day
-
P2 testing
Difficulty 1/5 Under an hour Newbie friendliness 90/100
Maintainers usually reply within 1 day
-
enhancement javascript
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
area/core kind/bug status/triage team/core-shared
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day