Maintenance
A repository does more than serve what you push to it. Over time it accumulates old versions you no longer resolve, it holds artifacts that were clean when admitted but have since had a CVE disclosed against them, and it grows a web of dependencies worth being able to query. Maintenance is the set of background passes that keep it in shape - and, like everything else in the repository, each pass is an optional plug-in an operator turns on, reading and writing only the object store, with no database or extra service to run.
The capability - a maintenance task and its lease
A maintenance task is a discovered capability. The server hosts one neutral scheduler that owns the worker thread, walks every tenant and repository, holds the single-writer lease, and collects the gauges. A task itself only says what to do per repository (and, optionally, tenant-wide work afterwards). So a new background pass is a drop-in module - the scheduler names no task, and picks up whichever ones are on its module path at start-up.
Each task reads its own enablement and interval from configuration. With no task enabled the scheduler
sits idle; with none installed the deployment simply has no background maintenance. A deployment can check
what it got at GET /api/capabilities, which reports each pass's installed and enabled state, and the
super-admin Modules console lists the same.
The single-writer lease
The server is stateless and replicated for availability, so a pass that mutates shared state - the
cleanup sweep, the dependents index - must run on one node at a time, or two nodes would sweep the same
store at once. A lease enforces that: a small locks/<name> object in the store, taken with a
compare-and-set write (a true cross-node CAS on an S3 or Azure backend, via the object's ETag). A crashed
holder's lease expires and is reclaimed with no coordinator, and a long pass renews its lease as it
runs so a rival is refused throughout.
Turning a pass on or off
Every pass is off until you enable it, and the scheduler re-resolves its task list live: flip a pass's setting and it joins or drops out of the worker's next iteration - no restart. That is the general rule for the mutating passes (cleanup, scan, dependents); the exact settings are in the table at the end.
Implementations
Retention and cleanup
Left alone, every version you ever published stays forever. A retention policy trims that back to what you actually want to keep. It is a rule over the published versions of each coordinate, with four dials that compose - a version is kept only if it satisfies all of the ones you set:
| Dial | Keeps a version when it is… |
|---|---|
keep-last |
among the N newest of its coordinate (0 disables the count cap). |
max-age |
published within this duration. |
prerelease-expiry |
a prerelease published within this duration (releases are unaffected). |
not-downloaded-for |
downloaded within this duration - the dial that evicts cold versions, which keeps a proxy cache lean. |
Two safety rules always hold: the single newest version of a coordinate is never evicted, so a cleanup can never empty a coordinate; and a pinned version (below) is never evicted, whatever the rules say. A policy is set per repository, or falls back to the deployment default.
The cleanup sweep (the cleanup task) applies it. Each pass reads each repository's policy, evicts the
versions it no longer keeps, garbage-collects the content blobs nothing points at any more, rolls up the
browse tree's cached folder sizes, and reconciles a quota'd tenant's usage counter. It is a mutating pass, so
it runs under the lease. Enable it with scheduled-cleanup and set its cadence with cleanup-interval.
You can also run it on demand - and preview it first. The admin surface (mirrored on the console's
per-repository maintenance screen, gated repository:read/repository:write) is:
GET /repository/{repo}/admin/cleanup/plan # what the current policy WOULD evict - nothing removed
POST /repository/{repo}/admin/cleanup # run it: evict per policy, then GC unreferenced blobs
GET /repository/{repo}/admin/retention # read the policy
PUT /repository/{repo}/admin/retention # set keep-last / max-age / prerelease-expiry / not-downloaded-for
501 - "retention is not installed" - rather than silently doing nothing. Where it
is installed, jenesis.repository.cleaner=false gates it off to the same
501, and the exclusive seam selects an engine by name with
jenesis.repository.retention (cleaner is the shipped implementation) - see
Feature
toggles & implementation selection.
Pins
A pin force-keeps one version, immune to every retention rule - the way you protect an LTS release or a
version a downstream build still resolves while a max-age sweep trims everything around it. Pin and unpin a
coordinate through POST/DELETE …/admin/pin (or the console's per-repository maintenance screen), and list
the current pins at …/admin/pins.
Vulnerability re-scan
The compliance gate decides at the moment of publish or proxy. It cannot answer the question that comes later: a CVE disclosed - or added to the known-exploited catalogue - after an artifact was already admitted. The re-scan answers exactly that. It walks a repository's already-published coordinates against the live advisory feed and reports each vulnerable one with its advisory id, severity, fixed version, whether it is known-exploited (on CISA's KEV catalogue) and its EPSS exploitation probability - ordering the report so a coordinate something in the repository actually depends on (per the dependents index, below) sorts above one merely scored in the abstract, and the known-exploited and high-probability ones sort to the top.
jenesis-repo vulnerabilities my-repo # re-scan what you already hold
The same scan is served at GET /api/vulnerabilities?repo= and on a per-repository Vulnerabilities
console panel. So the answer arrives without anyone asking: a scheduled re-scan (scheduled-scan,
cadence scan-interval-millis) re-sweeps every repository on a timer and publishes per-repository counts as
Micrometer gauges - jenesis.vulnerabilities.count and jenesis.vulnerabilities.known.exploited.count,
tagged by tenant and repository - logging a warning for any repository holding a known-exploited artifact.
A dashboard alert on the known-exploited gauge then fires the moment a CVE you already hold lands on the KEV
catalogue, with no scan to remember to run. This pass is read-only, so it takes no lease; each node
refreshes its own gauges.
kev-enforce; its kev-auto-hold switch is on by default) quarantines an
already-published artifact once its CVE lands on the known-exploited catalogue - the same
/quarantine hold and review queue the gate writes, and an operator's release sticks. Only the
actively-exploited set is ever auto-held; everything below KEV stays report-only, so a broad new CVE can
never mass-hold a repository. Its licence counterpart, license-retro-enforce, holds releases
against the licence policy the same way - opt-in, running only once its license-retro-hold
setting names an enforcement tier.
Dependents index
Search finds a coordinate; the dependents index answers the inverse - who depends on X, and therefore
the blast radius of a CVE. A parser reads the CycloneDX SBOM embedded in a stored artifact into a
dependency-edge model (streamed out of the jar, only the small BOM materialised), and a lease-guarded sweep
(the dependents task) inverts those edges into a sharded, compare-and-set index in the store - no
database, compacted each pass. A query surface reads it back without a scan:
jenesis-repo dependents my-repo com.acme:widget # who pulls in this coordinate
jenesis-repo dependents my-repo # the coordinates the index holds
The same answers serve GET /api/dependents and a console panel. Enable the sweep with dependents-index
and set its cadence with dependents-interval. On a deployment without the module the surface degrades
cleanly - a 501 and a hidden panel - and the re-scan simply loses its reachability ordering.
Volume reclamation
Retention trims by policy; reclamation is the operator's lever for disk pressure. A super-admin can
run a volume-wide disk reclaim across every tenant, deleting least-recently-used content until a
free-space target is met (min-free-bytes or min-free-percent). It is a cross-tenant, on-demand operation -
deliberately a super-admin concern rather than a per-tenant one - run from the console's instances screen.
Unlike the cleanup sweep, which removes only what a repository's own retention rules release, a reclaim is a
blunt free-space guarantee for when a bounded volume is filling up.
The shared artifact walk
Each pass above visits what it already knows how to find - a repository's versions, its stored SBOMs. A pass
that must visit everything the store holds rides a different primitive: the shared artifact walk, one
ordered, resumable enumeration of the store's keys that every store-sweeping consumer uses instead of writing
its own listing loop. The walk is itself a discovered capability - jenesis.repository.walk selects an
implementation by name, and the shipped store walk descends the store's own key layout through ordered
paging - and it hands every rider the same guarantees:
- It resumes, never restarts. Progress is committed as a compare-and-set cursor every
jenesis.walk.checkpointkeys (default1000). A node that dies mid-pass loses at most the uncommitted tail, which is re-visited on resume - so a consumer is written to tolerate seeing an item twice, and a pass over a huge store survives any interruption. - Replicas split the work without a coordinator. A pass is planned as up to
jenesis.walk.segmentscontiguous ranges (default32), and each node claims a segment with a compare-and-set write - a claim is refused, never stolen. A dead node's claim expires afterjenesis.walk.ttlseconds (default900) and another node resumes the segment from its last cursor. - State lives only in the store. Like everything else, a pass's manifest and cursors are objects in the object store - there is no scheduler database, and a pass survives the death of any process that ran it.
The walk also carries a rebuild seam: one enumeration of the published pointers can feed every installed walk consumer its retained items. A plug-in enabled late - a new index, a new gauge - back-fills its whole view from one shared pass rather than shipping a scan of its own.
Garbage collection
Content addressing splits removal in two. Evicting a version removes its pointer; the blob it pointed at may be shared with other versions, so blobs are reclaimed separately, once nothing references them. Beyond what the cleanup sweep releases, a store also accrues blobs no pass ever revisits - the orphan of a rejected or crashed upload, bytes whose every pointer is long gone. The garbage collector is the capability that finds and reclaims them.
Deleting data is the one unrecoverable act, so garbage collection is the most strictly opt-in capability in
the product: with no collector module installed, nothing is ever reclaimed - the standard image ships
without one, and its absence costs only disk. Installing one is a deliberate choice, and
jenesis.repository.gc selects among installed collectors by name; mark-sweep is the shipped
implementation.
The mark-sweep collector rides the shared artifact walk - never a listing loop of its own - and is built so
a live blob can never be deleted:
- Mark, sharded. A walk over the serving pointers records every referenced blob hash, flushed durably before each cursor commit - so a committed cursor never lies about a reference that was still sitting in a buffer. The sweep then reads the content-addressed namespace in hash order, one leading-byte shard at a time, so memory stays bounded on a store of any size.
- Condemn, then collect. A pass that finds an unreferenced blob does not delete it - it condemns it with a marker. Only a later pass that finds the blob still unreferenced deletes it, with the marker re-read immediately before the delete. Every blob therefore gets at least one full pass interval of grace.
- The write path cooperates. Landing a pointer - a publish, a promotion, a deduplicated re-deploy of bytes already stored - clears the blob's condemned marker, so content that becomes referenced again between passes is never collected.
- Only blobs are judged. The collector only ever evaluates content-addressed blob objects; every other object in the store - pointers, indexes, configuration - is untouched by construction.
- A dry run first. The collector distinguishes a read-only plan - what a collection would reclaim, writing nothing - from the collection itself, so a first run can be previewed before anything is deleted.
The walk and collector read startup keys, spelled in full (they are not per-repository dials):
| Key | Default | Meaning |
|---|---|---|
jenesis.repository.walk |
(first enabled - store) |
Selects the artifact-walk implementation store-sweeping passes enumerate through. |
jenesis.walk.checkpoint |
1000 |
Keys visited between durable cursor commits of a walk segment. |
jenesis.walk.segments |
32 |
Target number of ranges a pass is split into across nodes. |
jenesis.walk.ttl |
900 |
Seconds before a dead node's segment claim expires and its segment is resumed elsewhere. |
jenesis.repository.gc |
(none installed - nothing reclaimed) | Selects the garbage collector; mark-sweep is the shipped implementation. |
jenesis.gc.stride |
20000 |
Checkpoint stride of the collector's own walk passes. |
jenesis.gc.grace |
(none) | Optional ISO-8601 wall-clock floor on the condemn-to-collect grace, on top of the one-pass gap. Set it so a blob is never reclaimed until it has carried its condemned marker at least this long - a guard for when several nodes collect, or a node re-collects after a lease expiry, and generations advance faster than the collection interval. |
Settings
Every maintenance pass is off until you enable it. Each key below is a repository setting - pin it from above
the store with an environment variable or a -Djenesis.repository.<key>= system property, or set it on the
settings screen when its module is installed.
| Key | Default | Meaning |
|---|---|---|
scheduled-cleanup |
false |
Run the retention/cleanup sweep in the background. The on-demand cleanup endpoint works either way. |
cleanup-interval |
PT1H |
How often the cleanup sweep runs. |
keep-last |
0 |
Deployment-default retention: keep the N newest versions per coordinate (0 = no count cap). |
max-age |
(none) | Deployment-default retention: evict versions older than this duration. |
prerelease-expiry |
(none) | Deployment-default retention: expire prereleases older than this duration. |
not-downloaded-for |
(none) | Deployment-default retention: evict versions not downloaded within this duration. |
scheduled-scan |
false |
Re-scan every repository against the advisory feeds on a timer, publishing per-repository gauges. |
scan-interval-millis |
3600000 |
How often the scheduled re-scan runs, in milliseconds (one hour). |
kev-auto-hold |
true |
Whether the kev-enforce pass retroactively quarantines an already-published artifact once its CVE reaches the known-exploited catalogue. |
dependents-index |
false |
Build the reverse-dependency ("who depends on X") index in the background. |
dependents-interval |
PT1H |
How often the dependents sweep runs. |
cleanup-lease |
PT10M |
Time-to-live of the single-writer maintenance lease that keeps a mutating sweep on one node. |
Durations are ISO-8601 (PT1H is an hour, P90D ninety days, P14D a fortnight) - a blank retention
duration disables that rule. The one exception is scan-interval-millis, which is a plain millisecond count.
The volume-reclaim target is a console setting rather than a repository one: jenesis.ui.min-free-bytes
and jenesis.ui.min-free-percent set the free-space floor a super-admin's reclaim aims for.
Because every index and pointer these passes touch lives only in the scoped object store, there is nothing extra to back up: delete a derived index and the next sweep rebuilds it.