Conversation
Two option rows of the same poll can hold different stored hashes and still resolve to the same recalculated hash, i.e. after an option text or duration change that never got the hash rewritten, or with the empty default hash of an old install. UNIQ_options over poll_id, poll_option_hash and timestamp then rejects the second row's hash update, TableManager logs "Error updating option hash for optionId" and skips the row. Nothing repairs it, so JanitorCron produces the same error every night. deleteDuplicates() cannot see those pairs, because it compares the stored hashes and not the recalculated ones. The log noise is not the worst of it: updateVoteHashes() succeeds where updateOptionHashes() failed, so the votes move to the new hash while their option keeps the old one. JanitorCron then purges the deleted twin and runs removeOrphanedVotes(), which joins on poll_id and hash, and deletes the votes. Group options and votes by their recalculated key before the hashes are written and drop the redundant rows, keeping a live option over a deleted one and a confirmed one over an unconfirmed one, so a poll's result is never the row that gets dropped. Count the rows that could not be written and let JanitorCron skip the orphan sweep while that count is not zero, so a swallowed error cannot cascade into a vote deletion again. Two things that kept this from working on Oracle at all: Oracle stores an empty string as null, so the nullable hash columns read back as null and hydrating the entity threw a TypeError. That is an Error, not an Exception, so nothing caught it and the whole cron run died on exactly the legacy rows this repair is meant to fix. VoteMapper::update() reloads the entity through buildQuery(), which groups by the primary key while selecting all columns. Oracle rejects that with ORA-00979, so a vote hash could never be rewritten. The maintenance path has no use for the joined attributes, so write the hash without the reload. Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
solracsf
force-pushed
the
fix/option-hash-collision-cron
branch
from
September 6, 2026 21:06
4d223c1 to
c1c3037
Compare
Collaborator
|
Thanks for your try, but:
Your solution may work, but you are facing an inconsistency, which should be fixed once by recreating the database structure. After that this combination will never appear again, prevented by the unique index. Therefore a regularly fix job is not necessary. It is always a good idea to add an issue before adding a PR. So please make sure, that the issue is solvable by |
dartcafe
marked this pull request as draft
September 29, 2026 17:21
This branch has not been deployed
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.
The problem
JanitorCronlogsError updating option hash for optionId {id}every night, forever.Two option rows of the same poll can hold different stored hashes and still resolve to the same recalculated hash: after an option text or duration change that never rewrote the hash, or with the empty default hash of an old install.
UNIQ_options (poll_id, poll_option_hash, timestamp)rejects the second row's update,updateOptionHashes()catches, logs and skips it, and nothing repairs the row, so the next run does the same.deleteDuplicates()never sees those pairs, because it compares the stored hashes and not the recalculated ones.Worse than the log noise:
updateVoteHashes()succeeds whereupdateOptionHashes()failed, so the votes move to the new hash while their option keeps the old one. In the same runpurgeDeletedOptions()removes the deleted twin andremoveOrphanedVotes(), which joins onpoll_id+ hash, deletes those votes.updateHashes()already warns "Do not catch any exceptions ... Otherwise data loss of votes can occur" - the per-row catches were doing exactly that. Reproduced on MariaDB and PostgreSQL.The fix
Option::getTimestampInDB(), because the unique index keys on the storedtimestampcolumn, not ongetTimestamp(), which is derived fromiso_timestamp.Two Oracle-only bugs made the repair impossible there.
''is stored as null, so the nullable hash columns hydrated null into aprotected stringand threw aTypeError- anError, not anException, so nothing caught it and the whole cron run died on exactly the legacy rows this repair targets. AndVoteMapper::update()reloads throughbuildQuery(), which groups by the primary key while selecting all columns, which Oracle rejects with ORA-00979; the maintenance path has no use for the joined attributes, soupdateHash()writes without the reload.Tests
New
tests/Unit/Db/TableManagerTest.php, 7 cases, red before and green after. Reverting any one of the three non-obvious decisions (getTimestampInDB(), the confirmed tie-break, the nullable hash property) makes exactly one of them fail.Worth knowing when reviewing:
occ app:enable pollsdoes not create the unique indices, soocc polls:db:reset-unique-indicesis needed for any test to exercise the constraint.Affected instances are repaired by the next cron run;
occ polls:db:rebuildfixes them now.The content of this PR was fully reviewed using AI