Update link creation for use Java API instead of ln or mklink commands#172
Conversation
|
Thanks for starting this overdue cleanup @melissalinkert. Since Java 8 is the minimal requirement (and soon Java 11), switch to I'll start listing a set scenarios that should be evaluated functionally including error conditions. From an initial reading of the proposed code changes, a few high-level API questions:
|
Deprecates `createProcessBuilder`, and uses `exec(File, File)` instead.
|
Last few commits are one approach to the comments in #172 (comment). I don't have a good sense of the impact of wider refactoring/API changes here, so had originally stuck with the existing API to be safe. |
sbesson
left a comment
There was a problem hiding this comment.
Using a synthetic test file, tested the main transfer types: ln, ln_s, cp and cp_rm with the omero import command. Under the appropriate filesystem requirements, confirmed that the import worked as expected.
Tested the error condition by using --transfer=ln with a file on a different filesystem
$ omero import --transfer=ln test.fake
...
2026-03-23 12:18:06,404 1950 [2-thread-1] INFO ormats.importer.cli.LoggingImportMonitor - FILESET_UPLOAD_PREPARATION
2026-03-23 12:18:06,598 2144 [2-thread-1] INFO ormats.importer.cli.LoggingImportMonitor - FILESET_UPLOAD_START
2026-03-23 12:18:06,617 2163 [3-thread-1] INFO .importer.transfers.HardlinkFileTransfer - Transferring /mnt/data/seb/test.fake...
2026-03-23 12:18:06,657 2203 [3-thread-1] INFO ormats.importer.cli.LoggingImportMonitor - FILE_UPLOAD_STARTED: /mnt/data/seb/test.fake
2026-03-23 12:18:06,672 2218 [3-thread-1] ERROR ome.formats.importer.cli.ErrorHandler - FILE_EXCEPTION: /mnt/data/seb/test.fake
java.nio.file.FileSystemException: /OMERO/ManagedRepository/import.user_2/2026-03/23/12-18-06.472/test.fake -> /mnt/data/seb/test.fake: Invalid cross-device link
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:100)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.createLink(UnixFileSystemProvider.java:481)
at java.base/java.nio.file.Files.createLink(Files.java:1102)
at ome.formats.importer.transfers.HardlinkFileTransfer.exec(HardlinkFileTransfer.java:44)
at ome.formats.importer.transfers.AbstractExecFileTransfer.transfer(AbstractExecFileTransfer.java:65)
at ome.formats.importer.ImportLibrary.uploadFile(ImportLibrary.java:531)
at ome.formats.importer.ImportLibrary$3.call(ImportLibrary.java:634)
at ome.formats.importer.ImportLibrary$3.call(ImportLibrary.java:631)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
2026-03-23 12:18:06,674 2220 [2-thread-1] ERROR ome.formats.importer.ImportLibrary - Error on import
java.nio.file.FileSystemException: /OMERO/ManagedRepository/import.user_2/2026-03/23/12-18-06.472/test.fake -> /mnt/data/seb/test.fake: Invalid cross-device link
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:100)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.createLink(UnixFileSystemProvider.java:481)
at java.base/java.nio.file.Files.createLink(Files.java:1102)
at ome.formats.importer.transfers.HardlinkFileTransfer.exec(HardlinkFileTransfer.java:44)
at ome.formats.importer.transfers.AbstractExecFileTransfer.transfer(AbstractExecFileTransfer.java:65)
at ome.formats.importer.ImportLibrary.uploadFile(ImportLibrary.java:531)
at ome.formats.importer.ImportLibrary$3.call(ImportLibrary.java:634)
at ome.formats.importer.ImportLibrary$3.call(ImportLibrary.java:631)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
2026-03-23 12:18:06,675 2221 [2-thread-1] INFO ome.formats.importer.ImportLibrary - Exiting on error
==> Summary
0 files uploaded, 0 filesets created, 0 images imported, 1 error in 0:00:00.277
Also tested functionally by using a real-world example HCS plate with 648 files. The fileset was imported 3 consecutive times using cp or ln_s transfer and omero-blitz 5.8.4 or omero-blitz 5.8.5-SNAPSHOT. The first result was discarded to account for any filesystem caching.
| cp | ln_s | |
|---|---|---|
| omero-blitz 5.8.4 | 33.29s +/- 0.17s | 47.17s +/- 0.37s |
| omero-blitz 5.8.5-SNAPSHOT | 33.20s +/- 0.04s | 46.31s +/- 0.28s |
A few proposed inline changes in the AbstractExecFileTransfer class to avoid this counting as a backwards incompatible change but otherwise this looks good, simplifies the code and the maintenance and causes no noticeable performance degradation.
In addition to the many files scenario, I'll run a last functional test with a large single file.
| throw new RuntimeException(msg); | ||
| } | ||
| } | ||
| protected abstract void exec(File file, File location) throws IOException; |
There was a problem hiding this comment.
Although I think converting this into an abstract method is probably the right approach, this would have the impact of breaking any concrete implementation other than the ones shipped with omero-blitz.
To avoid calling this a backwards-compatible change, proposing to keep the current implementation in AbstractExecFileTransfer but override it in our subclasses and suggest other implementations should do so.
...with a comment that implementations should override this method.
sbesson
left a comment
There was a problem hiding this comment.
Rested using a 2G file (LuCa-7color_Scan1.ome.tiff) that was imported with the three primary local file transfer mechanisms as described above
| cp | ln | ln_s | |
|---|---|---|---|
| omero-blitz 5.8.5 | 1.34s +/- 0.06s | 0.15s +/- 0.01s | 0.15s +/- 0.01s |
| omero-blitz 5.8.5-SNAPSHOT | 1.70s +/- 0.06s | 0.15s +/- 0.01s | 0.15s +/- 0.01s |
There is a slight performance degradation associated with using Files.copy. From experience, in most scenarios where a file can be directly copied from its original location into the binary repository, a linking strategy (soft or link) is usually preferred. Alternatively, under some conditions a ln_rm strategy will move the data under the binary repository.
From my perspective, the benefits of using the standard Java library vastly exceed this performance difference. This is amplified by the fact it only represents a small percentage of the total import time.
With the latest changes, the current AbstractExcecFileTransfer.exec implementations is unmodified, AbstractExcecFileTransfer.printLine which duplicated the superclass implementation is cleaned up and AbstractExcecFileTransfer.createProcessBuilder contains a default implementation and is deprecated.
The three local file transfer implementation are updated in a backwards compatible manner to use java.nio. One last comment about a Javadoc marker and this should be ready for integration in the next release of this component
| * @param file File to be copied. | ||
| * @param location Location to copy to. | ||
| * @throws IOException | ||
| * @param throws IOException |
Would this be a problem for e.g. HCS data with thousands of files though? |
#172 (review) tested the HCS scenario with ~500 1-2MB files with no evidence of an overhead associated. |
|
Ah, apologies, I missed that you had already tested that case. My concern was that if the difference of 0.36s in the |
Agreed. I haven't seen any example of filesets containing 5-10K 2G files but I'll look into something intermediate with multiple files in the 100-500MB range |
kkoz
left a comment
There was a problem hiding this comment.
This is definitely a step in the right direction. I don't have any issues with the code as such, but it still feels awkward. Some of the existing structure implies there is still execution of command line functions, which there aren't. Should we open an issue to clean this up or is it not really worth it?
|
Adding another real-world HCS example with the idr0093 CV7000 plate (14099 files, 146G) imported via symlink with one upload thread with/without this PR
|
I definitely agree that there is more cleanup that could be done, e.g. 5f12456. Since this PR is intended to be backwards-compatible, though, capturing as an issue makes sense. |
sbesson
left a comment
There was a problem hiding this comment.
Ran a final import test using idr0056 (311 files, 23G total) and transfer=cp
| transfer=cp | |
|---|---|
| 5.8.4 | 45.54s |
| 5.8.5-SNAPSHOT | 43.23s |
Overall, these results suggest that the proposed changes do not significantly impact the transfer / import times across a variety of input and transfer types.
Imports with
--transfer=lnor--transfer=ln_smay be a bit faster, but otherwise this should have no functional impact.