Repository navigation
[#171] Skip CSV snapshot rows without a uid and delete the snapshots a failed sync copied - #183
Conversation
…e sync delete pass A snapshot row without a uid was never reported, because a live row without one fails the sync before any delta. The delete pass now skips it with a warning instead of failing after earlier DELETEs were sent, so a sync from such a snapshot reaches handleResult and the token moves.
sync() copies the live file before the diff when the token is null or names a missing snapshot, and again after the diff when it reports changes. When the sync fails before the handler accepts the token, it now deletes the copies it made, under the write lock. A kept copy could be returned by getLatestSyncToken, and it stopped a retry on the unchanged live file: sync(null) failed to copy over it, and a sync from the old snapshot reported the same changes again without moving the token. A snapshot named by the caller's token is never deleted. getLatestSyncToken takes only the read lock, so a call that overlaps the failing sync can still return a copy's token before the copy is deleted. A sync from that token then starts over from the live file, as it does for any token without a snapshot.
…ng its token was handled Every delta of a sync carries the token of the snapshot copied before the diff. A consumer that stores the token of each handled delta resumes from that snapshot after a failure, so deleting it made the next sync start over from the live file and silently skip the changes in between. The copy is now kept as soon as the handler returns from a delta; it is still deleted when the handler fails on the first one.
…efore its read section scrubSyncFiles() runs after the pre-diff copy, still under the write lock, while the cleanup that deletes a failed sync's copies belongs to the read section after it. When it threw (File.list() returns null on an I/O error), the copy stayed, and the next sync(null) on the unchanged file failed with "Unable to copy CSV file for sync operation". Also pin the delete-pass copies.clear(): a live file whose columns are permuted after init yields DELETEs from a fresh copy, because the connector reads the live file with the header cached at init.
vharseko
left a comment
There was a problem hiding this comment.
nit (non-blocking): SyncOpDiffTest.java:337 — syncThatCannotListItsSnapshotsDeletesTheSnapshotItCopied expects exactly NullPointerException, which is what the unchecked dataDir.list() in scrubSyncFiles happens to throw today. The test is about the cleanup, not the exception type: once #174 reworks scrubSyncFiles (e.g. a ConnectorIOException on an unlistable directory), this test goes red although the copy is still deleted. expectThrows(RuntimeException.class, ...) would keep it pinned to the cleanup only.
Not raised here, as they are already tracked: the start-copy collision and the token kept when <csv>.<mtime> exists (#174), snapshot retention (#174), the handler-stop semantics (#175), and the external-writer race (#172).
…le-directory sync test syncThatCannotListItsSnapshotsDeletesTheSnapshotItCopied expected the NullPointerException that scrubSyncFiles() happens to throw on an unlistable directory, so a rework of that method (OpenIdentityPlatform#174) would turn it red although the copy is still deleted. It now accepts any RuntimeException. A RuntimeException alone also accepts a sync that fails before the copy is made, where the empty directory proves nothing. The test sets the directory's mtime to 0 before the sync and asserts that it moved, which only creating or deleting an entry does.
|
Re review 5493443471: agreed, the test is about the cleanup and the NPE is incidental. Changing only the expected type would open another gap, though: 535f9c4 widens the expectation to
Without mutants the test is green, and so is the whole module (122 tests, 0 skipped). The "red at cb7965d" cell in the description still holds: at cb7965d the test fails on the leftover copy. |
vharseko
left a comment
There was a problem hiding this comment.
Re 6140592007: thanks — the mtime witness closes the gap I missed in my own suggestion; RuntimeException alone would indeed have accepted the pre-copy ConnectorException with nothing to clean up.
Checked 535f9c4 locally (macOS, JDK 21):
SyncOpDiffTest: 34 tests, 0 failures, 0 skipped (the POSIX test ran).ConnectorExceptionthrown beforeFiles.copy("copy fails first"): red, on the new assertionThe sync failed before it copied the CSV file.ConnectorIOExceptionon an unlistable directory inscrubSyncFiles(#174-like): green.
Both match your table. No further findings.
659e8b0
into
OpenIdentityPlatform:master
Fixes #171.
CSVFileConnector.sync()no longer fails on a snapshot row without a uid, and a failed sync no longer leaves behind the snapshots it copied.handleResult. The token moves with the next change; until then every poll logs the warning again.sync()copies the live file before the diff when the token isnullor names a missing snapshot, and again after the diff when it reports changes. When the sync fails before the handler accepts the token, it now deletes the copies it made, under the write lock. A kept copy could be returned bygetLatestSyncToken, and it stopped a retry on the unchanged live file:sync(null)failed with "Unable to copy CSV file for sync operation", and a sync from the old snapshot reported the same changes again without moving the token. A snapshot named by the caller's token is never deleted, and neither is the copy made before the diff once the handler has returned from a delta: every delta carries that copy's token, and a consumer that stores the token of each handled delta resumes from it.getLatestSyncTokentakes only the read lock, so a call that overlaps the failing sync can still return a copy's token before the copy is deleted. A sync from that token then starts over from the live file, as it does for any token without a snapshot, and never reports the changes made since that token. A sync from that token that takes the write lock before the copy is deleted runs from the copy, but if it reports no change it hands back the same token, and the sync after it starts over.Test strength
CSVFileConnector.java:577delete pass,nulluid → skipsnapshotRowWithoutUidIsSkipped[",Jane,Doe,pw1"]snapshotRowWithoutUidIsSkipped["\" \",Jane,Doe,pw1"]uid == nullinstead ofStringUtil.isBlankskippedSnapshotRowIsLoggedCSVFileConnector.java:681finallydeletes the copyfailedSyncDeletesTheSnapshotItCopiedCSVFileConnector.java:497same when the sync fails before it holds the read locksyncThatCannotListItsSnapshotsDeletesTheSnapshotItCopied(POSIX, non-root)failedSyncKeepsTheSnapshotOfItsTokensyncOriginsuccessfulSyncKeepsTheSnapshotItCopied!completedcompletedafterhandleResultsyncWhoseTokenIsNotDeliveredDeletesTheSnapshotItCopiedcompletedset before the token blockCSVFileConnector.java:670the copy made after the diff is trackedsyncWhoseTokenIsNotDeliveredDeletesTheSnapshotItCopiedAfterTheDiffcopiesCSVFileConnector.java:638a handled delta keeps the copyfailedSyncKeepsTheSnapshotOfADeltaItHandledcopies.clear()handlereturnssyncWhoseFirstDeltaFailsDeletesTheSnapshotItCopiedcopies.clear()beforehandleCSVFileConnector.java:588same in the delete passfailedSyncKeepsTheSnapshotOfADeleteItHandled(relies on the header cached atinit: the live columns are permuted after it)copies.clear()deleteSnapshotwarningsnapshotThatCannotBeDeletedIsLogged(POSIX, non-root)delete()ignoreddeleteSnapshotwrite lockreadLock().unlock()andwriteLock().lock(), a window a test cannot open or hold without the private static lock map. The lock keeps a copy from being deleted while another sync reads it; it does not stopgetLatestSyncTokenfrom returning the copy's token earlier