Repository navigation
[#172] Diff sync() against a copy of the CSV file and keep that copy as the next snapshot - #193
Conversation
… and keep that copy as the next snapshot sync() copied the live file after the diff, so a row an external writer deleted during the sync was in neither the diff nor the next snapshot and was never reported. The copy is now taken under the write lock before the diff, the index and create/update passes read it, and once a delta is accepted it is moved to <csv>.<mtime read before the copy>.
… to the copy taken before the diff
…t mode and the unreadable CSV file of sync(), and log the copy a pass fails to read
vharseko
left a comment
There was a problem hiding this comment.
CSVFileConnector.java:500: the copy of the CSV file does not need the write lock
next is a private .tmp file. Every writer in this JVM takes the write lock, so the read lock already keeps them out while the copy is taken. Holding the write lock for the copy instead blocks every search, get, getLatestSyncToken and the start of other syncs for the whole copy. Before this PR, that happened only on a sync with a null or stale token. Now it happens on every sync, including one from a valid token on an unchanged file. That path used to hold the write lock only for an exists() check.
Taking nextTimestamp and copyCsvFile() at the start of the read-locked section would avoid this, and needs no new error path: the finally at :698-710 already deletes copies and next when the sync fails. It would also take the second full copy that a sync with a null or stale token now makes (:489, then :500) out of the write-locked section.
…d lock instead of the write lock The read lock keeps every writer in this JVM out of the copy, as the write lock did, while searches, gets and getLatestSyncToken keep running. The write-locked section is back to what it was before the copy was added. copyOfTheCsvFileBlocksWritersButNotReaders pins both sides.
|
Re review 5515753375: fixed in 2aa8447.
The move does not let another sync start earlier: it starts with the write lock, which waits for the read lock of the first sync until that sync ends. Following that through, the POSIX |
vharseko
left a comment
There was a problem hiding this comment.
Re 2aa8447: the copy now runs at the start of the read-locked section, and the write-locked section is back to what it is on master. A failed copy is cleaned up by copyCsvFile() itself, and the origin copy of a null or stale token by the finally of the read section. getLatestSyncToken, which can now run during the copy, does not pick up the .tmp file, because its pattern requires the name to end in 13 digits. copyOfTheCsvFileBlocksWritersButNotReaders kills both lock mutants. The corrected Files.move trade-off and the new table row match the code. The module passes locally: 132 tests, 0 failures, 0 skipped.
Non-blocking, wording only: the lock from fileNameToLockMap is a non-fair ReentrantReadWriteLock. Once a writer in this JVM queues behind the sync, a search that starts after it waits behind that writer: for the rest of the copy and for the diff. "Searches do not wait for the copy" therefore holds until a writer queues. The diff on master already behaves this way, so this is not a regression and needs no code change. The comment at :512 and the Trade-offs line could say so, if you want them exact.
…writer waits for the sync copy The lock from fileNameToLockMap is a non-fair ReentrantReadWriteLock, which still makes a new reader wait behind a queued writer. Once a writer in this JVM queues behind the sync, a search that starts after it waits for the rest of the copy and for the diff. Comment only; the line count is unchanged.
|
Re review 5535943732: the wording is fixed in f1f2df9. The comment at
|
vharseko
left a comment
There was a problem hiding this comment.
Re f1f2df9: the comment at CSVFileConnector.java:512-514 now says that a reader which starts after a writer has queued also waits for the copy. The body says the same in the read-lock bullet, in the first Trade-offs line and in the :517 row of the table. All three match the code. Every reader in the connector takes the lock with lock() (:367, :723, :869), not with the barging tryLock(), so the non-fair queue check applies to it. On JDK 21 I see the same behaviour as the reply describes: while a read lock is held and a writer is queued, a new readLock().lock() waits until both have released the lock, and a reader that arrives before the writer gets in at once.
The commit changes only the comment. Its three lines stay within 120 columns, and the line numbers in the body still point at the statements they name. copyOfTheCsvFileBlocksWritersButNotReaders still tests the case it names: it awaits its getLatestSyncToken before it submits the delete. The module passes locally at f1f2df9: 132 tests, 0 failures, 0 skipped. CI is green.
Fixes #172.
CSVFileConnector.sync()now copies the CSV file under the read lock before the diff, diffs the token's snapshot against that copy, and keeps the copy as the next snapshot. A change that an external writer makes while a sync is running is now reported by the next sync instead of being lost.Before this change, the index pass and the create/update pass read the live file. After the diff, if a delta had been accepted, the live file was copied to
<csv>.<lastModified>as the next snapshot. External writers take no lock. A row deleted after the index pass was therefore in neither the diff nor the next snapshot, and no sync ever reported its DELETE.getLatestSyncTokenkeep running until a writer in this JVM queues behind the sync: the lock is a non-fairReentrantReadWriteLock, which still makes a reader that starts after that wait behind the writer, for the rest of the copy and for the diff. Readers already wait this way during the diff.copyCsvFile()writes the copy to a<csv>.<digits>.tmpfile fromcreateRewriteFile(), and deletes that file if the copy fails. The copy gets the mode of the CSV file, as snapshots do. Exception messages still name the CSV file, and the error log of a pass that reads the copy names the copy as well. A sync from a token still fails withConnectorIOException"File … does not exist" when the CSV file is missing.<csv>.<mtime>withFiles.movewithout options. If that snapshot already exists, the existing snapshot and the token stay, as before. In every other case the copy is deleted when the sync ends, whether it succeeded or failed.copies.clear()after a handled delta ([#171] Skip CSV snapshot rows without a uid and delete the snapshots a failed sync copied #183);nulltoken;Trade-offs:
nullor stale token copies it twice: to its origin, as before, and for the diff. Writers in this JVM wait for the copy, as they wait for the diff. A search waits for the copy only if it starts after a writer has queued behind the sync, as it then waits for the diff. A sync of an unchanged file now fails withConnectorException"Unable to copy CSV file for sync operation" if the connector cannot write to the directory or the disk is full. Before, such a sync returned no delta and failed only at the first change.ConnectorIOException"File … does not exist". A CSV file that exists but cannot be read, or an I/O error while it is read, now fails withConnectorException"Unable to copy CSV file for sync operation", as a sync without a token already did. Before, these wereConnectorIOException"File … does not exist" and "Error reading from file …".Files.movechecks the target first and then renames, so it does not arbitrate between two syncs that reach the move with the same mtime: both can pass the check, and the second sync then replaces the first one's snapshot. Both copies were taken from the file at that mtime. If the second sync then fails inhandleResult, it deletes the snapshot that the first sync handed out. This takes two syncs of one CSV file that do not share a lock: in two JVMs, or in one JVM through two absolute paths of the file, such as a symlink. Two syncs of one path in one JVM run one after another, because each starts with the write lock, which waits for the read lock of the other.<csv>.<digits>.tmpfiles by hand while no connector is running against the CSV file.<csv>.<mtime>whose content differs from the live file at that mtime (CSVFile connector: sync() never deletes old snapshots, and fails or keeps the old token when the current file already has a snapshot #174);false(CSVFile connector: sync() keeps offering deltas after the handler returns false and moves the token past the deltas it did not accept #175).Test strength
CSVFileConnector.java:635the create/update pass reads the copyrowCreatedDuringSyncIsReportedByTheNextSyncOnlyCSVFileConnector.java:680the copy becomes the next snapshotrowDeletedDuringSyncIsReportedByTheNextSyncCSVFileConnector.java:676same, once a delta is acceptedrowDeletedDuringSyncIsReportedByTheNextSync,rowCreatedDuringSyncIsReportedByTheNextSyncOnly,acceptedDeltaAdvancesTheTokenCSVFileConnector.java:550the index pass reads the copycsvFileChangedAfterItsCopyIsReportedByTheNextSync(overridescopyCsvFile()to rewrite the live file once the copy is taken)CSVFileConnector.java:515the mtime is read before the copycopyCsvFile()CSVFileConnector.java:517the copy is taken under the read lock, so writers in this JVM wait for it, and a reader that starts before any writer queues does notcopyOfTheCsvFileBlocksWritersButNotReaders(overridescopyCsvFile()to callgetLatestSyncToken, thendelete, on another thread)CSVFileConnector.java:680an existing snapshot is not replacedexistingSnapshotOfTheCopiedMtimeKeepsTheTokenREPLACE_EXISTINGCSVFileConnector.java:685FileAlreadyExistsExceptionkeeps the tokenCSVFileConnector.java:707the copy is deleted when the sync endsexistingSnapshotOfTheCopiedMtimeKeepsTheToken,successfulSyncKeepsTheSnapshotItCopiedand 6 moreCSVFileConnector.java:1313copyCsvFile()deletes its file when the copy failsmissingCsvFileFailsSync,unreadableCsvFileFailsSyncCSVFileConnector.java:688a copy that cannot be moved to the next snapshot fails the syncsnapshotThatCannotBeMovedInFailsSync(POSIX, non-root)CSVFileConnector.java:1309the copy, and so the next snapshot, takes the mode of the CSV filesnapshotTakesTheModeOfTheCsvFile(POSIX, skipped when the umask gives the CSV file rw-------)createTempFilefile in place, which keeps rw-------CSVFileConnector.java:521an unreadable CSV file fails the copy with "Unable to copy…"unreadableCsvFileFailsSync(POSIX, non-root)AccessDeniedExceptionsent to theConnectorIOExceptionarmCSVFileConnector.java:566,:661a pass that cannot read the copy names the CSV file in its exception and the copy in its logcopyDeletedBeforeTheIndexPassFailsSyncNamingTheCsvFile,copyDeletedBeforeTheCreateUpdatePassFailsSyncNamingTheCsvFileCSVFileConnector.java:682next = nullafter the move.tmpfile thatcreateTempFilegave the freed name; a test cannot make it hand out that namemvn -pl OpenICF-csvfile-connector test: 132 run, 0 failures, 0 skipped (122 at 78c1e16).