This document describes why locking is necessary in the tus-java-server library, how the core UploadLockingService interface is structured, how lock wait and contention resolution works across replicas, and where to find detailed documentation for each concrete locking mechanism implementation.
In the tus protocol (and IETF Resumable Uploads for HTTP specification), client uploads can be interrupted and resumed across multiple HTTP requests. Multiple concurrent requests targeting the same upload resource must be strictly prevented to avoid data corruption (such as out-of-order byte writes or overlapping file offsets).
- Active Streaming: When a client sends upload bytes via a
PATCH(or RUFHPOST/PATCH) request, the server acquires an exclusive lock on that upload. - Network Interruption: If the client's network drops, the original
PATCHconnection may remain open on the server in a "half-open" state (a stalled socket read waiting for client bytes). - Resume Attempt: The client, recognizing the disconnect, attempts to resume by sending a
HEADrequest to query the current offset (or aDELETErequest to terminate the upload). - Lock Conflict & Lock Wait: The stalled
PATCHrequest is still running on the server and holding the lock. Rather than failing immediately, the server enters a lock wait retry loop (retrying every 200ms up towithLockWaitTimeout(Duration), default 60s / 300 retries). - Contention Resolution: The waiting request calls
lockingService.requestLockRelease(requestUri), which signals the active upload to interrupt its input stream. - Drain & Handover: For cloud backends (S3, Azure), the interrupted upload is allotted a background drain timeout (
lockWaitTimeout - 5 seconds, default 55s) to safely flush in-flight chunks and persist updated byte offsets before releasing the lock. Once freed, the waiting request acquires the lock and immediately returns the accurate offset to the client.
All locking mechanisms in tus-java-server implement the UploadLockingService interface and return handles implementing UploadLock.
public interface UploadLockingService {
// Acquires an exclusive lock on an upload resource
UploadLock lockUploadByUri(String requestUri) throws TusException, IOException;
// Checks if an upload is currently locked
boolean isLocked(UploadId id);
// Cleans up stale or expired locks (fallback sweeper)
void cleanupStaleLocks() throws IOException;
// Cleans up the lock file or object for a completed, terminated, or expired upload
default void cleanupLock(UploadId id) throws IOException {}
// Cleans up the lock file or object for a completed, terminated, or expired upload by URI
default void cleanupLock(String uploadUri) throws IOException {}
// Injects the UploadIdFactory instance used to parse upload IDs from request URIs
void setIdFactory(UploadIdFactory idFactory);
// Registers the active request input stream so it can be interrupted cleanly
default void registerInputStream(String requestUri, InputStream inputStream) {}
// Requests that any active lock for the URI be released
default void requestLockRelease(String requestUri) {}
// Closes resources, interrupts in-flight streams, and shuts down background daemon threads
default void close() throws IOException {}
}public interface UploadLock extends Closeable {
// Gets the request URI associated with this lock
String getUploadUri();
// Explicitly releases the lock
default void release() {
try {
close();
} catch (IOException ignored) {}
}
}- Stream Registration: When a request starts streaming payload bytes, its input stream is wrapped in an
InterruptibleInputStreamand registered vialockingService.registerInputStream(requestUri, inputStream). - Automatic Lock Cleanup on Completion: When an upload finishes (
offset == length),TusFileUploadServiceautomatically invokeslockingService.cleanupLock(uploadId)to remove the lock file/blob (.lock) and any stop signal file/blob (.stop). - Automatic Lock Cleanup on Termination & Expiration: When an upload is canceled via
DELETE(204 No Content) or expired viacleanupExpiredUploads(), the associated lock file or blob is also automatically deleted vialockingService.cleanupLock(uploadId). Actively held locks are defended and never deleted mid-stream. - Fallback Stale Lock Sweeper: As a safety net,
cleanupStaleLocks()periodically sweeps orphaned lock records (e.g. unleased Azure blobs older than 120s or stop signal markers older than 30s) left behind after hard node crashes or power outages. - Release Request: When a concurrent
HEADorDELETErequest encounters an active lock,TusFileUploadServicecatchesUploadAlreadyLockedExceptionand callslockingService.requestLockRelease(requestUri). - Stream Interruption:
- If the lock is held in the same JVM,
requestLockReleaseinterrupts the local stream directly. - If the lock is held on a remote replica/pod,
requestLockReleasewrites a.stopsignal object or file. A background watchdog thread running on the lock-holding replica detects the.stopsignal and callsstream.interrupt(), causing the stalledPATCHstream to abort and release its lock.
- If the lock is held in the same JVM,
tus-java-server provides several built-in locking implementations tailored for different deployment topologies and storage backends. Refer to the dedicated documentation files below for full details:
| Storage Backend / Environment | Locking Service Class | Key Characteristics & Architecture | Documentation File |
|---|---|---|---|
| Disk & Network Filesystems (Default) | LeaseFileLockingService |
Atomic sibling mutex directory (<UploadId>.mutex/), in-place expired lock takeover, TTL-based JSON lease files with heartbeat renewal, ownership fencing, and .stop signal files. Fully safe on NFSv3/v4, AWS EFS, SMB/CIFS, Kubernetes containers, and local disks. |
docs/DISK_BASED_LOCKING.md |
| Local File System (Legacy Opt-Out) | DiskLockingService |
OS kernel-level exclusive POSIX FileLock (fcntl) with JVM shutdown hooks and .stop signal files. Best for single-node deployments on local disk. |
docs/DISK_BASED_LOCKING.md |
| Amazon S3 / S3-Compatible | S3LockingService |
S3 object-backed TTL lease objects (.lock), atomic conditional writes (If-None-Match: *), non-CAS lock arbitration with configurable jitter backoff (withJitter) for backends lacking CAS (B2, Ceph, Wasabi), heartbeat renewal, and cross-pod .stop signal object polling watchdog. |
docs/S3_STORAGE.md |
| Azure Blob Storage | AzureBlobLockingService |
Native Azure Blob Storage exclusive 30-second leases (BlobLeaseClient), background daemon renewal, and .stop signal blob polling watchdog. |
docs/AZURE_BLOB_STORAGE.md |
Developers extending tus-java-server with custom lock providers (such as Redis, ZooKeeper, etcd, or Hazelcast) must implement UploadLockingService and UploadLock:
- Implement
lockUploadByUri: Acquire an exclusive lock or throwUploadAlreadyLockedExceptionif currently locked by another request. - Implement
registerInputStream&requestLockRelease: Maintain a registry of activeInterruptibleInputStreaminstances. WhenrequestLockReleaseis invoked, interrupt the active stream to support instant client resumes. - Use Common Watchdog Helpers: Use
Utils.scheduleWatchdog(...)andUtils.shutdownExecutor(...)inme.desair.tus.server.util.Utilsfor any background daemon threads or heartbeat lease renewals. - Implement
Closeable: Register a JVM shutdown hook upon construction and deregister it onclose()for idempotent resource cleanup. - Guard Against TOCTOU Races: When building distributed lock services with TTL expiration, ensure acquisition and eviction use atomic operations (e.g., conditional writes, CAS, atomic directory renames, or post-action verification) to prevent race conditions where multiple nodes concurrently evict an expired lock and simultaneously acquire ownership.