• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 39491797049

20 Oct 2026 08:25PM UTC coverage: 94.308% (-0.08%) from 94.39%
39491797049

Pull #145

github

web-flow
Merge 1ce3476f3 into ab81e0ce1
Pull Request #145: feat: cloud locking performance hardening, expired upload cleanup, and conformity child deduplication

3366 of 3834 branches covered (87.79%)

Branch coverage included in aggregate %.

516 of 531 new or added lines in 29 files covered. (97.18%)

16 existing lines in 7 files now uncovered.

7139 of 7305 relevant lines covered (97.73%)

6.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.84
/src/main/java/me/desair/tus/server/upload/cache/ThreadLocalCachedStorageAndLockingService.java
1
package me.desair.tus.server.upload.cache;
2

3
import java.io.IOException;
4
import java.io.InputStream;
5
import java.io.OutputStream;
6
import java.lang.ref.WeakReference;
7
import java.time.Duration;
8
import java.util.Objects;
9
import me.desair.tus.server.checksum.ChecksumAlgorithm;
10
import me.desair.tus.server.checksum.ChecksumKey;
11
import me.desair.tus.server.exception.TusException;
12
import me.desair.tus.server.exception.UploadNotFoundException;
13
import me.desair.tus.server.upload.UploadId;
14
import me.desair.tus.server.upload.UploadIdFactory;
15
import me.desair.tus.server.upload.UploadInfo;
16
import me.desair.tus.server.upload.UploadLock;
17
import me.desair.tus.server.upload.UploadLockingService;
18
import me.desair.tus.server.upload.UploadStorageService;
19
import me.desair.tus.server.upload.concatenation.UploadConcatenationService;
20

21
/**
22
 * Combined implementation of {@link UploadStorageService} and {@link UploadLockingService}. Uses
23
 * both of them as delegates but allowing to reduce disk operations during a request processing by
24
 * caching UploadInfo in the memory. UploadLockingService service is used as a delegate to cleanup
25
 * cached data on releasing a lock.
26
 */
27
public class ThreadLocalCachedStorageAndLockingService
28
    implements UploadLockingService, UploadStorageService {
29

30
  private final ThreadLocal<WeakReference<UploadInfo>> uploadInfoCache = new ThreadLocal<>();
10 ✔
31
  private final UploadLockingService lockingServiceDelegate;
32
  private final UploadStorageService storageServiceDelegate;
33
  private UploadIdFactory idFactory;
34

35
  /** Constructor of ThreadLocalCachedStorageAndLockingService. */
36
  public ThreadLocalCachedStorageAndLockingService(
37
      UploadStorageService storageServiceDelegate, UploadLockingService lockingServiceDelegate) {
4 ✔
38
    if (storageServiceDelegate.getClass() == ThreadLocalCachedStorageAndLockingService.class) {
8 ✔
39
      this.storageServiceDelegate =
12 ✔
40
          ((ThreadLocalCachedStorageAndLockingService) storageServiceDelegate)
41
              .storageServiceDelegate;
42
    } else {
43
      this.storageServiceDelegate = storageServiceDelegate;
6 ✔
44
    }
45
    if (lockingServiceDelegate.getClass() == ThreadLocalCachedStorageAndLockingService.class) {
8 ✔
46
      this.lockingServiceDelegate =
12 ✔
47
          ((ThreadLocalCachedStorageAndLockingService) lockingServiceDelegate)
48
              .lockingServiceDelegate;
49
    } else {
50
      this.lockingServiceDelegate = lockingServiceDelegate;
6 ✔
51
    }
52
  }
2 ✔
53

54
  @Override
55
  public UploadInfo getUploadInfo(UploadId id) throws IOException {
56
    if (id == null) {
4 ✔
57
      return null;
4 ✔
58
    }
59
    UploadInfo uploadInfo;
60
    WeakReference<UploadInfo> ref = uploadInfoCache.get();
10 ✔
61
    if (ref == null || (uploadInfo = ref.get()) == null || !id.equals(uploadInfo.getId())) {
26 ✔
62
      uploadInfo = storageServiceDelegate.getUploadInfo(id);
10 ✔
63
      uploadInfoCache.set(new WeakReference<>(uploadInfo));
14 ✔
64
    }
65
    return uploadInfo;
4 ✔
66
  }
67

68
  @Override
69
  public UploadInfo getUploadInfo(String uploadUrl, String ownerKey) throws IOException {
70
    UploadInfo uploadInfo = getUploadInfo(idFactory.readUploadId(uploadUrl));
14 ✔
71
    if (uploadInfo == null || !Objects.equals(uploadInfo.getOwnerKey(), ownerKey)) {
14 ✔
72
      uploadInfo = storageServiceDelegate.getUploadInfo(uploadUrl, ownerKey);
12 ✔
73
      uploadInfoCache.set(new WeakReference<>(uploadInfo));
14 ✔
74
    }
75
    return uploadInfo;
4 ✔
76
  }
77

78
  @Override
79
  public void update(UploadInfo uploadInfo) throws IOException, UploadNotFoundException {
80
    storageServiceDelegate.update(uploadInfo);
8 ✔
81
    uploadInfoCache.set(new WeakReference<>(uploadInfo));
14 ✔
82
  }
2 ✔
83

84
  @Override
85
  public void setIdFactory(UploadIdFactory idFactory) {
86
    this.idFactory = idFactory;
6 ✔
87
    this.storageServiceDelegate.setIdFactory(idFactory);
8 ✔
88
    this.lockingServiceDelegate.setIdFactory(idFactory);
8 ✔
89
  }
2 ✔
90

91
  @Override
92
  public String getUploadUri() {
93
    return storageServiceDelegate.getUploadUri();
8 ✔
94
  }
95

96
  @Override
97
  public UploadInfo append(UploadInfo upload, InputStream inputStream)
98
      throws IOException, TusException {
99
    UploadInfo info = storageServiceDelegate.append(upload, inputStream);
12 ✔
100
    uploadInfoCache.set(new WeakReference<>(info));
14 ✔
101
    return info;
4 ✔
102
  }
103

104
  @Override
105
  public void setMaxUploadSize(Long maxUploadSize) {
106
    storageServiceDelegate.setMaxUploadSize(maxUploadSize);
8 ✔
107
  }
2 ✔
108

109
  @Override
110
  public long getMaxUploadSize() {
111
    return storageServiceDelegate.getMaxUploadSize();
8 ✔
112
  }
113

114
  @Override
115
  public void setMaxAppendSize(Long maxAppendSize) {
116
    storageServiceDelegate.setMaxAppendSize(maxAppendSize);
8 ✔
117
  }
2 ✔
118

119
  @Override
120
  public Long getMaxAppendSize() {
121
    return storageServiceDelegate.getMaxAppendSize();
8 ✔
122
  }
123

124
  @Override
125
  public void setMinAppendSize(Long minAppendSize) {
126
    storageServiceDelegate.setMinAppendSize(minAppendSize);
8 ✔
127
  }
2 ✔
128

129
  @Override
130
  public Long getMinAppendSize() {
131
    return storageServiceDelegate.getMinAppendSize();
8 ✔
132
  }
133

134
  @Override
135
  public void setMinSize(Long minSize) {
136
    storageServiceDelegate.setMinSize(minSize);
8 ✔
137
  }
2 ✔
138

139
  @Override
140
  public Long getMinSize() {
141
    return storageServiceDelegate.getMinSize();
8 ✔
142
  }
143

144
  @Override
145
  public UploadInfo create(UploadInfo info, String ownerKey) throws IOException {
146
    UploadInfo uploadInfo = storageServiceDelegate.create(info, ownerKey);
12 ✔
147
    uploadInfoCache.set(new WeakReference<>(uploadInfo));
14 ✔
148
    return uploadInfo;
4 ✔
149
  }
150

151
  @Override
152
  public InputStream getUploadedBytes(String uploadUri, String ownerKey)
153
      throws IOException, UploadNotFoundException {
154
    return storageServiceDelegate.getUploadedBytes(uploadUri, ownerKey);
12 ✔
155
  }
156

157
  @Override
158
  public InputStream getUploadedBytes(UploadId id) throws IOException, UploadNotFoundException {
159
    return storageServiceDelegate.getUploadedBytes(id);
10 ✔
160
  }
161

162
  @Override
163
  public void copyUploadTo(UploadInfo info, OutputStream outputStream)
164
      throws UploadNotFoundException, IOException {
165
    storageServiceDelegate.copyUploadTo(info, outputStream);
10 ✔
166
    uploadInfoCache.set(new WeakReference<>(info));
14 ✔
167
  }
2 ✔
168

169
  @Override
170
  public void cleanupExpiredUploads() throws IOException {
171
    storageServiceDelegate.cleanupExpiredUploads();
6 ✔
172
    // Since any cached uploads was potentially removed by the storage service
173
    // we clean the cache to prevent any stale state
174
    cleanupCache();
4 ✔
175
  }
2 ✔
176

177
  @Override
178
  @Deprecated
179
  public void cleanupExpiredUploads(UploadLockingService uploadLockingService) throws IOException {
180
    storageServiceDelegate.cleanupExpiredUploads(uploadLockingService);
4 ✔
181
    // Since any cached uploads was potentially removed by the storage service
182
    // we clean the cache to prevent any stale state
183
    cleanupCache();
2 ✔
184
  }
1 ✔
185

186
  @Override
187
  public void removeLastNumberOfBytes(UploadInfo uploadInfo, long byteCount)
188
      throws UploadNotFoundException, IOException {
189
    storageServiceDelegate.removeLastNumberOfBytes(uploadInfo, byteCount);
10 ✔
190
    uploadInfoCache.set(new WeakReference<>(uploadInfo));
14 ✔
191
  }
2 ✔
192

193
  @Override
194
  public void terminateUpload(UploadInfo uploadInfo) throws UploadNotFoundException, IOException {
195
    storageServiceDelegate.terminateUpload(uploadInfo);
8 ✔
196
    // Since the upload is terminated and potentially removed by the storage service
197
    // we clean the cache to prevent any stale state
198
    cleanupCache();
4 ✔
199
  }
2 ✔
200

201
  @Override
202
  public Long getUploadExpirationPeriod() {
203
    return storageServiceDelegate.getUploadExpirationPeriod();
8 ✔
204
  }
205

206
  @Override
207
  public void setUploadExpirationPeriod(Long uploadExpirationPeriod) {
208
    storageServiceDelegate.setUploadExpirationPeriod(uploadExpirationPeriod);
8 ✔
209
  }
2 ✔
210

211
  @Override
212
  public void setUploadConcatenationService(UploadConcatenationService concatenationService) {
213
    storageServiceDelegate.setUploadConcatenationService(concatenationService);
8 ✔
214
  }
2 ✔
215

216
  @Override
217
  public UploadConcatenationService getUploadConcatenationService() {
218
    return storageServiceDelegate.getUploadConcatenationService();
8 ✔
219
  }
220

221
  @Override
222
  public void setUploadDeduplicationEnabled(boolean enabled) {
223
    storageServiceDelegate.setUploadDeduplicationEnabled(enabled);
8 ✔
224
  }
2 ✔
225

226
  @Override
227
  public boolean isUploadDeduplicationEnabled() {
228
    return storageServiceDelegate.isUploadDeduplicationEnabled();
8 ✔
229
  }
230

231
  @Override
232
  public void setJsonSerializationEnabled(boolean enabled) {
233
    storageServiceDelegate.setJsonSerializationEnabled(enabled);
4 ✔
234
  }
1 ✔
235

236
  @Override
237
  public boolean isJsonSerializationEnabled() {
238
    return storageServiceDelegate.isJsonSerializationEnabled();
4 ✔
239
  }
240

241
  @Override
242
  public UploadInfo getUploadInfoByChecksum(String checksum, ChecksumAlgorithm algorithm)
243
      throws IOException {
244
    return storageServiceDelegate.getUploadInfoByChecksum(checksum, algorithm);
12 ✔
245
  }
246

247
  @Override
248
  public UploadInfo deduplicate(UploadInfo uploadInfo, ChecksumKey checksumKey) throws IOException {
UNCOV
249
    UploadInfo result = storageServiceDelegate.deduplicate(uploadInfo, checksumKey);
×
UNCOV
250
    if (result != null) {
×
UNCOV
251
      uploadInfoCache.set(new WeakReference<>(result));
×
252
    }
UNCOV
253
    return result;
×
254
  }
255

256
  @Override
257
  public void setUploadLockingService(UploadLockingService uploadLockingService) {
258
    storageServiceDelegate.setUploadLockingService(uploadLockingService);
4 ✔
259
  }
1 ✔
260

261
  @Override
262
  public UploadLockingService getUploadLockingService() {
263
    return storageServiceDelegate.getUploadLockingService();
4 ✔
264
  }
265

266
  @Override
267
  public boolean dataExists(UploadId id) throws IOException {
268
    return storageServiceDelegate.dataExists(id);
5 ✔
269
  }
270

271
  @Override
272
  public void deleteData(UploadId id) throws IOException {
273
    storageServiceDelegate.deleteData(id);
4 ✔
274
  }
1 ✔
275

276
  @Override
277
  public void setCloudUploadThreadPoolSize(int size) {
278
    storageServiceDelegate.setCloudUploadThreadPoolSize(size);
8 ✔
279
  }
2 ✔
280

281
  @Override
282
  public int getCloudUploadThreadPoolSize() {
283
    return storageServiceDelegate.getCloudUploadThreadPoolSize();
8 ✔
284
  }
285

286
  @Override
287
  public void setDrainTimeout(Duration drainTimeout) {
288
    storageServiceDelegate.setDrainTimeout(drainTimeout);
8 ✔
289
  }
2 ✔
290

291
  @Override
292
  public Duration getDrainTimeout() {
293
    return storageServiceDelegate.getDrainTimeout();
8 ✔
294
  }
295

296
  @Override
297
  public UploadLock lockUploadByUri(String requestUri) throws TusException, IOException {
298
    UploadLock uploadLock = lockingServiceDelegate.lockUploadByUri(requestUri);
10 ✔
299
    return new CachedLock(uploadLock);
12 ✔
300
  }
301

302
  @Override
303
  public UploadLock lockUpload(UploadId id) throws TusException, IOException {
304
    UploadLock uploadLock = lockingServiceDelegate.lockUpload(id);
5 ✔
305
    return new CachedLock(uploadLock);
6 ✔
306
  }
307

308
  @Override
309
  public void cleanupStaleLocks() throws IOException {
310
    lockingServiceDelegate.cleanupStaleLocks();
6 ✔
311
    cleanupCache();
4 ✔
312
  }
2 ✔
313

314
  @Override
315
  public boolean isLocked(UploadId id) {
316
    return lockingServiceDelegate.isLocked(id);
5 ✔
317
  }
318

319
  @Override
320
  public void registerInputStream(String requestUri, InputStream inputStream) {
321
    lockingServiceDelegate.registerInputStream(requestUri, inputStream);
10 ✔
322
  }
2 ✔
323

324
  @Override
325
  public void requestLockRelease(String requestUri) {
326
    lockingServiceDelegate.requestLockRelease(requestUri);
8 ✔
327
  }
2 ✔
328

329
  @Override
330
  public void cleanupLock(UploadId id) throws IOException {
331
    lockingServiceDelegate.cleanupLock(id);
4 ✔
332
    cleanupCache();
2 ✔
333
  }
1 ✔
334

335
  @Override
336
  public void close() throws IOException {
337
    lockingServiceDelegate.close();
3 ✔
338
    storageServiceDelegate.close();
3 ✔
339
    cleanupCache();
2 ✔
340
  }
1 ✔
341

342
  private void cleanupCache() {
343
    WeakReference<UploadInfo> ref = uploadInfoCache.get();
10 ✔
344
    if (ref != null) {
4 ✔
345
      uploadInfoCache.remove();
6 ✔
346
      ref.clear();
4 ✔
347
    }
348
  }
2 ✔
349

350
  class CachedLock implements UploadLock {
351

352
    private final UploadLock delegate;
353

354
    CachedLock(UploadLock delegate) {
10 ✔
355
      this.delegate = delegate;
6 ✔
356
    }
2 ✔
357

358
    @Override
359
    public String getUploadUri() {
360
      return delegate != null ? delegate.getUploadUri() : null;
8 !
361
    }
362

363
    @Override
364
    public void release() {
365
      try {
366
        if (delegate != null) {
3 !
367
          delegate.release();
3 ✔
368
        }
369
      } finally {
370
        // Clean up thread-local upload info cache when release() is called
371
        // to prevent ThreadLocal memory leaks across pooled servlet threads.
372
        cleanupCache();
3 ✔
373
      }
374
    }
1 ✔
375

376
    @Override
377
    public void close() throws IOException {
378
      try {
379
        if (delegate != null) {
6 ✔
380
          delegate.close();
6 ✔
381
        }
382
      } finally {
383
        cleanupCache();
6 ✔
384
      }
385
    }
2 ✔
386
  }
387
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc