The implementation adds tenant-aware assets/jobs plus a configurable per-tenant active-job quota. The main enforcement is in src/main/java/org/example/videoclips/application/VideoAssetService.java:165, backed by tenant fields
added to the domain/persistence model and migration src/main/resources/db/migration/V6__tenant_quota_support.sql:1. Requests can set X-Tenant-Id on asset creation, quota config lives in src/main/java/org/example/videoclips/
config/VideoClippingProperties.java:39, and quota violations now return 429 via src/main/java/org/example/videoclips/api/ApiExceptionHandler.java:44.
VideoAssetService.java, src/main/java/org/example/videoclips/storage/ObjectStoragePort.java, src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java, and src/main/java/org/
example/videoclips/storage/S3ObjectStorageAdapter.java.
I also added focused coverage for this slice:
- API test for generating a signed URL for an actual produced clip in src/test/java/org/example/videoclips/api/VideoAssetControllerTest.java
- cleanup job tests for expired clip/object deletion and disabled cleanup in src/test/java/org/example/videoclips/operations/RetentionCleanupJobTest.java
- src/main/java/org/example/videoclips/domain/VideoAsset.java
- src/main/java/org/example/videoclips/domain/Clip.java
That metadata is mapped through the in-memory and JPA repository paths, including:
- src/main/java/org/example/videoclips/infrastructure/InMemoryVideoAssetRepository.java
- src/main/java/org/example/videoclips/persistence/JpaVideoClippingRepository.java
- src/main/java/org/example/videoclips/persistence/entity/VideoAssetEntity.java
- src/main/java/org/example/videoclips/persistence/entity/ClipEntity.java
I also added scheduled retention cleanup in src/main/java/org/example/videoclips/operations/RetentionCleanupJob.java, with configuration in src/main/java/org/example/videoclips/config/
VideoClippingProperties.java and src/main/resources/application.properties. Right now it soft-expires assets by marking them deleted when their retention time passes. Clip retention timestamps
are now assigned by both clipper adapters so the service has real expiration data to work with.
What this does not do yet is physically delete expired clip objects or original source objects from object storage. The retention job is metadata-aware now, but object-store deletion still needs
to be added through the storage port as the next step.
- the FFmpeg input directory
- the FFmpeg output directory
- tmp/in-memory-storage
- tmp/stub-output
The schedule and retention window are now configurable through src/main/java/org/example/videoclips/config/VideoClippingProperties.java and src/main/resources/application.properties via:
- video-clipping.cleanup.enabled
- video-clipping.cleanup.local-artifact-poll-interval-ms
- video-clipping.cleanup.local-artifact-retention-hours
This closes the first retention slice from the plan on the worker side. It does not yet delete expired original videos or clips from real object storage, and it does not do database-driven
retention policies. That would be the next cleanup step.
The root cause was that ClipProcessor had been updated to call objectStoragePort.uploadGeneratedClip(...), but src/main/java/org/example/videoclips/storage/ObjectStoragePort.java did not yet
declare that method. I added it to the port and confirmed both adapters implement it:
- src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java
- src/main/java/org/example/videoclips/storage/S3ObjectStorageAdapter.java
I also verified the generated-clip contract is aligned across:
- src/main/java/org/example/videoclips/processing/GeneratedClip.java
- src/main/java/org/example/videoclips/processing/VideoClipperPort.java
- src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java
- src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java
java/org/example/videoclips/persistence/entity/ClipEntity.java and src/main/java/org/example/videoclips/persistence/JpaVideoClippingMapper.java, and added the schema migration at src/main/
resources/db/migration/V5__clip_object_key.sql.
The clipper contract now returns both metadata and the local file path for each generated clip through src/main/java/org/example/videoclips/processing/GeneratedClip.java and the updated src/main/
java/org/example/videoclips/processing/VideoClipperPort.java. Both adapters were updated:
- src/main/java/org/example/videoclips/processing/StubVideoClipperAdapter.java now creates stub local output files
- src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java now returns deterministic local output paths for generated segments
On the storage side, ObjectStoragePort gained uploadGeneratedClip(...), with implementations in:
- src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java
- src/main/java/org/example/videoclips/storage/S3ObjectStorageAdapter.java
And src/main/java/org/example/videoclips/processing/ClipProcessor.java now uploads each generated local file through storage before saving the clip record, so the job path is finally staged input
-> process -> uploaded output -> persisted clip metadata.
I also exposed objectKey in the clip response via src/main/java/org/example/videoclips/application/VideoAssetService.java and extended the MockMvc test to assert it exists in src/test/java/org/
example/videoclips/api/VideoAssetControllerTest.java.
- materializeSourceObject(String sourceObjectKey, Path targetPath) in src/main/java/org/example/videoclips/storage/ObjectStoragePort.java
Both storage adapters implement it:
- src/main/java/org/example/videoclips/storage/InMemoryObjectStorageAdapter.java creates a placeholder local file for the default stub flow
- src/main/java/org/example/videoclips/storage/S3ObjectStorageAdapter.java downloads the object from S3-compatible storage to the worker filesystem
The processor now uses that seam before clip generation starts. In src/main/java/org/example/videoclips/processing/ClipProcessor.java it resolves the local input path under the configured FFmpeg
input directory, materializes the source object there, and records a SOURCE_MATERIALIZED job event before invoking the clipper.
That means the FFmpeg path is no longer relying on a file appearing magically in the input directory. The next concrete step is to finish the output side the same way: persist generated clip
object keys and push FFmpeg outputs back through the storage port instead of only creating clip metadata in memory.