Skip to content

Latest commit

 

History

History
93 lines (71 loc) · 20.7 KB

File metadata and controls

93 lines (71 loc) · 20.7 KB

Stability — v1.x guarantees

S4 ships under the Semantic Versioning contract as of v1.0.0. That means the items below are stable for the v1.x line — any incompatible change to them ships under a v2.0.0 release with migration guidance, not a v1.x patch.

What's stable (= v2.0 if broken)

Surface Frozen at v1.0
Wire formats on the backend S4F2 framed body + S4P1 padding (multipart + single-PUT framed objects); S4IX v1 / v2 / v3 sidecar layouts; S4E1 / S4E2 / S4E3 / S4E4 / S4E5 / S4E6 SSE envelopes. A v1.x reader can read any byte stream another v1.x server has written, in either direction. Cross-major back-compat caveats: (a) v0.8.x readers handle S4IX v1 / v2 but return UnsupportedVersion(3) on v3 sidecars (introduced in v0.9 #106 for SSE-S4 chunked / S4E6 partial-fetch); deployments without an SSE-S4 keyring configured (= --sse-s4-key* flags unset) never emit v3 sidecars and are bidirectionally compatible with v0.8.x. The default --sse-chunk-size is 1 MiB and IS active whenever SSE-S4 is enabled, so SSE-S4 deployments DO emit v3 by default. (b) S4E6 was introduced in v0.8.1 (commit a7333f2), so any v0.8.1+ reader recognizes it; only the v0.8.0 hot-fix line lacks S4E6 support and would refuse SSE-S4 chunked objects. (c) v0.8.x server binaries can still read all v1.0-written framed bodies + v1/v2 sidecars + S4E1–S4E5 envelopes — the only cross-major refusals are the two listed above.
s4 binary subcommands (CLI surface) verify-sidecar, repair-sidecar, sweep-orphan-sidecars, verify-audit-log, plus the long-running server's documented --<flag> set. New flags are additive (default off).
s4_server::repair::* public API verify_sidecar, repair_sidecar (and the _with_keyring variant), sweep_orphan_sidecars. Types: RepairError, SidecarStatus, RepairReport, OrphanReason, OrphanReport, SweepReport, VerifyReport, DeletePolicy, RepairSseBinding. All public enums in this module are #[non_exhaustive] — adding a new variant in a minor release is not breaking (downstream match must use a catch-all arm). Public structs (RepairReport, OrphanReport, SweepReport, VerifyReport, RepairSseBinding) are NOT #[non_exhaustive]; their public field set is frozen as-is, additions to those structs are v2.0 territory. Library consumers can pin s4-server = "1" and rebuild against any v1.x without code changes.
s4_server::service::S4Service shape The S4Service<B> struct itself, its S4Service::new(backend, registry, dispatcher) constructor (signature: pub fn new(backend: B, registry: Arc<CodecRegistry>, dispatcher: Arc<dyn CodecDispatcher>) -> Self), and its builder API are frozen. The builder API is the long-form S4Service::new(...).with_<knob>(value)... chain — every pub fn with_* currently visible on S4Service (e.g. with_sse_key, with_sse_keyring, with_sse_chunk_size, with_secure_transport, with_trust_x_forwarded_for, with_max_body_bytes, with_sigv4a_gate, with_kms_backend, with_replication, with_replication_max_concurrent, with_versioning, with_object_lock, with_mfa_delete, with_cors, with_lifecycle, with_inventory, with_notifications, with_tagging, with_policy, with_access_log, with_rate_limits, with_compliance_strict, with_allow_legacy_reserved_key_reads) is locked to its current fn(self, …) -> Self signature; renames or signature changes ship under v2.0. Adding a new with_<knob> builder is additive (ships in a minor). The SharedService newtype at s4_server::service_arc::SharedService (the externally-supported "wrap an S4Service for clone-able shared use" path), SigV4aGate + SigV4aGateError, resolve_range, the DEFAULT_MAX_BODY_BYTES + DEFAULT_REPLICATION_MAX_CONCURRENT constants, and the wrapping pattern (Arc<S4Service> is the supported handle shape) are frozen. Implementation internals behind S4Service (request routing, multipart state, etc.) remain refactorable as long as the listed surface stays bit-equivalent at the call site. Important caveat on builder parameter types: 13 of the listed with_* builders take Arc<ManagerType> parameters whose ManagerType lives in an unfrozen module (e.g. with_tagging(Arc<tagging::TagManager>), with_inventory(Arc<inventory::InventoryManager>), with_replication(Arc<replication::ReplicationManager>), …). The builder signature is frozen — calling with_tagging with an Arc<TagManager> is contract-stable; but the TagManager constructor / fields / behavior are NOT frozen because s4_server::tagging is not in the freeze list (see §"Modules NOT in the freeze list" below). Library consumers who construct + inject these managers should pin a precise =1.x.y and treat the manager module surface as a manual-integration step across minors.
s4_server::sse public surface Types: SseKey, SseKeyring, SharedSseKey (= Arc<SseKey>, parameter type of S4Service::with_sse_key), SharedSseKeyring (= Arc<SseKeyring>), SseError, SseSource<'a>, S4E6Header<'a> (return type of parse_s4e6_header). Functions: compute_key_md5, encrypt, decrypt, encrypt_v2, parse_s4e6_header, peek_magic. Constants: SSE_C_ALGORITHM, ALGO_AES_256_GCM, SSE_MAGIC_V5, S4E5_HEADER_BYTES, S4E6_HEADER_BYTES. New SSE envelopes (e.g. provisional S4E7 chunked-KMS) ship as additive symbols and do not break the v1.x contract.
s4_server::streaming public surface DEFAULT_S4F2_CHUNK_SIZE constant, streaming_compress_to_frames + streaming_compress_to_frames_with functions. These functions accept s3s::dto::StreamingBlob parameters; that type is governed by the s3s 0.13 row (see "HTTP API surface" below).
s4-codec codec trait + format constants Codec trait shape, CodecKind enum (all #[non_exhaustive]), CodecError, IndexError, FrameError, GpuSelectError, CompareOp. Constants: index::{SIDECAR_SUFFIX, MAX_FRAMES, MAX_ETAG_BYTES, ENTRY_BYTES, HEADER_FIXED_V1, HEADER_FIXED_V2, INDEX_VERSION, INDEX_VERSION_V1, INDEX_VERSION_V2}. Items: index::{FrameIndex, encode_index, decode_index, FrameIndexEntry, SseChunkBinding, RangePlan, EncryptedRangePlan} (FrameIndex and the latter four are all pub structs; their public field sets + inherent method signatures are frozen at v1.0; field additions / removals / renames are v2.0 territory, same rule as the public structs in s4_server::repair), multipart::FrameHeader layout. Python (s4-codec-py) and WASM (s4-codec-wasm) bindings are versioned in lockstep with s4-codec; their binding-specific public APIs are frozen. The Python module s4_codec froze this guaranteed minimum export set at v1.0 (additive exports ship in minor releases and join the frozen set once shipped — examples: the v1.1 read helpers CpuZstdDict / read_frame / frame_iter / decode_index / crc32c + format constants + S4FrameError / S4IndexError, and the v1.2 write helpers encode_s4_object / bind_index / pick_chunk_size): classes CpuZstd + CpuGzip (Python-side names per the #[pyclass(name = "…")] attributes; the underlying Rust types are PyCpuZstd / PyCpuGzip), function gpu_available(), attribute __version__, and the exception classes S4Error, S4CrcMismatchError, S4SizeMismatchError, S4CodecMismatchError, S4UnregisteredCodecError, S4ManifestSizeExceedsLimitError, S4ManifestSizeMismatchError, S4BackendError, S4IoError (full hierarchy in ../crates/s4-codec-py/src/lib.rs:52-60). The WASM module exports exactly these names: decompressFramed, decompressSingle, supportedCodecs, supportedFrameMagic. The bindings do NOT re-export the full Rust surface — the contract for each binding is the names listed above plus the additively shipped minor-release exports recorded in the CHANGELOG.
s4-config The CompressionMode enum + BackendConfig struct field set + S4Config struct field set are frozen (the same pub use s4_config as config re-export inside s4-server makes these reachable through s4_server::config::*). The S4Config::from_toml stub is NOT frozen — it currently returns bail!("toml loading not implemented yet") and the eventual real implementation may change its error / return shape in any v1.x minor.
HTTP API surface S3 wire compatibility — the s3s 0.13 trait set S4 implements. PUT / GET / Range GET / multipart / SigV4 / SigV4a / x-amz-checksum-* / x-amz-server-side-encryption-* headers all preserved. s3s is itself pre-1.0; our v1.x contract is that we will continue to track the s3s 0.13 trait surface that S4 currently implements, accepting backward-compatible additions in s3s minors. A s3s major bump (0.14, 1.0) that breaks our trait impls would itself trigger a v2.0 of S4 with a clear migration in docs/migration/.
Container image tags + Helm chart values.yaml keys ghcr.io/abyo-software/s4:<major>.<minor>.<patch> + :<major>.<minor> + :latest floating tag rules; GPU build sibling tags :<major>.<minor>.<patch>-gpu. The complete top-level values.yaml key set is frozen: replicas, image.{repository, tag, pullPolicy, pullSecrets}, nameOverride, fullnameOverride, serviceAccount.{create, annotations, name}, backend.{endpointUrl, region}, codec, zstdLevel, dispatcher, logFormat, otlpEndpoint, gpu.{enabled, count, nodeSelector, runtimeClassName}, tls.{enabled, cert, key, existingSecret, certKey, keyKey}, policy.{json, existingConfigMap}, service.{type, port, annotations}, ingress.{enabled, className, annotations, hosts, tls}, resources.{requests, limits}, podAnnotations, podLabels, podSecurityContext, securityContext, nodeSelector, tolerations, affinity, extraEnv, extraVolumes, extraVolumeMounts, probes.{liveness, readiness}, marketplace.{productCode} (additive in chart 0.3.0 / s4 v1.3 — frozen from the release that ships it). Default values may shift in a minor release (e.g. a probe tuning change to reduce flake); the key shape (key names + structure) is v2.0 territory.

How to read the freeze table — scope of "frozen"

The freeze table above lists items by name. The v1.0 contract is exactly the named items, no more and no less:

  • Items named in the table are frozen. Their signatures, field sets (for structs), and variant sets (for #[non_exhaustive] enums modulo additive variants) are stable across all v1.x releases.
  • Other pub items in the same frozen modules are NOT part of the v1.0 contract. Each frozen module (s4_server::repair, s4_server::sse, s4_server::service, s4_server::streaming, s4_server::service_arc, s4_codec::index, s4_codec::multipart, etc.) carries other pub symbols (helper functions, internal constants, intermediate types) that exist because Rust visibility allows internal callers + integration tests to reach them. Examples currently present but NOT frozen: s4_server::repair::parse_bucket_key, the DEFAULT_REPAIR_BODY_BYTES_CAP / MAX_SIDECAR_BODY_BYTES / SSE_S4_REPAIR_MAX_OVERHEAD_BYTES / SSE_S4_REPAIR_MAX_CHUNK_SLACK_BYTES constants in repair; the SSE_MAGIC_V1V6 constants, CustomerKeyMaterial, parse_customer_key_headers, encrypt_with_source, S4E4Header, parse_s4e4_header, decrypt_with_kms, and the various chunked-SSE helpers in s4_server::sse; INDEX_MAGIC, SSE_BLOCK_V3, INDEX_HEADER_BYTES, build_index_from_body, sidecar_key, is_reserved_sidecar_key, FRAME_MAGIC, PADDING_MAGIC, FRAME_HEADER_BYTES in s4_codec. (This is a representative list, not exhaustive.)
  • If you depend on an unlisted item, pin a precise =1.x.y (not ^1) and treat each minor bump as a manual integration step. If you'd like an item promoted to the named freeze list, please file an issue with the use case.

Why this scope shape? An exhaustive "freeze every pub item" contract would over-promise on transitive internal-helper churn that the binary + tests need to be able to evolve. A "freeze nothing" contract would under-promise on the items library consumers actually integrate against. Naming the items keeps the contract explicit on both ends.

Modules NOT in the freeze list

s4-server ships 34 pub mod declarations from ../crates/s4-server/src/lib.rs so the s4 binary (which is a separate crate) + the integration tests + the example binaries can reach the surface they need. Five modules contribute frozen items above: repair, service, sse, streaming, and service_arc (the last contributes only SharedService; the rest of service_arc's contents are not frozen).

Library consumers MAY use s4_server::<other_module>::*; — Rust visibility allows it — but those imports are not frozen and may break in any v1.x minor release without notice. The other 30 modules (access_log, acme, audit_log, blob, cors, dict, estimate, inventory, kms, ledger, lifecycle, lock_recovery, metrics, mfa, migrate, multipart_state, notifications, object_lock, policy, rate_limit, recompact, replication, routing, select, sigv4a, state_loader, streaming_checksum, tagging, tls, versioning) exist as pub mod for binary-and-tests' needs, not as a published surface.

If you depend on one of these unfrozen modules, pin a precise =1.x.y (rather than ^1) and treat any minor bump as a manual integration step. If you would like an item promoted to the frozen surface, please file an issue with the use case.

Backend compatibility matrix (CI-verified surface)

What the freeze asserts about backends is the CI-gating status, not a "should work" claim. If a backend isn't listed as ✓ gating in the backend compatibility matrix, treat S4's other compat claims for it as "should work given S3 wire compatibility, not asserted by CI on this repo." The full per-backend table (MinIO gating; AWS S3 / B2 / R2 / Wasabi opt-in once secrets are configured; Garage / Ceph RGW continue-on-error) and the weekly compat-matrix.yml round-trip methodology are the single source of truth — they live in compatibility.md.

What's not promised (operator-tunable / explicitly opt-in)

  • Compression ratios + throughput numbers: these are workload-dependent and benchmark conditions are published, not promised SLAs.
  • Default values for --max-body-bytes, --sse-chunk-size, --gpu-min-bytes, and similar runtime tunables: defaults may shift in a minor release if a clear correctness / safety reason warrants it (the v0.9 #106-32bit fix that clamped to isize::MAX on 32-bit is an example of a default the SemVer-stable contract did not protect).
  • Implementation details inside frozen modules (private functions, struct field reordering, internal trait impls): the v1.0 freeze pins the items listed above, not "every line in service.rs". Re-arranging request-routing internals is fine in a minor.
  • Backend behavior beyond S3-wire-spec compliance (e.g. how a specific backend handles a particular SigV4 edge case): we test the documented backends (see §"Backend compatibility matrix"), but breakage caused by a backend-side change is not a v2.0 trigger on our end.
  • Experimental flags marked --allow-legacy-* or surfaced as unstable in --help: explicitly opt-in to behavior that may change.
  • Cross-region replication and the replication.* config surface: shipped as experimental scaffolding in v0.6 with the wire path stubbed in but no production-grade reconciliation. Excluded from v1.0 freeze; promotion to first-class (with Jepsen-class consistency tests) is on the v1.x roadmap below.
  • Security advisories accepted as risk-with-mitigation: see docs/security/cargo-audit-ignores.md for the 4 currently-ignored RUSTSEC advisories, each with rationale, mitigation, and upstream-tracking links. The ignore list is part of CI (cargo audit is a merge-block); changes to the list are visible in the diff.

v1.x roadmap candidates (= shipping under v1.x without breaking the contract above)

  • Chunked SSE-KMS envelope (provisional S4E7 magic) + chunked SSE-C (S4E8) → Range GET partial-fetch fast-path for SSE-KMS / SSE-C, parallel to the v0.9 #106 work that enabled it for SSE-S4 chunked (S4E6).
  • S4F3 streaming frame format → enables streaming PUT checksum verify for multipart upload_part (= closes the codec-API constraint documented in docs/security/streaming-checksum-coverage.md).
  • 32-bit s4-server runtime promotion from advisory to required CI smoke (currently advisory per v0.11 #A4).
  • Per-action SHA pinning on the GHA workflows (supply chain hardening; v0.11 #A5 ended at the floating-major tag policy).
  • Cross-region replication promoted from experimental scaffolding to production-grade, with Jepsen-style consistency tests.
  • Re-introducing Garage + Ceph as ✓ gating in the backend compat matrix once the upstream signature-interop drifts are resolved.
  • Additional codec backends (Snappy, LZ4 if user demand emerges).
  • v1.2 audit follow-ups (open, recorded for tracking): a skipped_unaccounted Prometheus gauge for the savings ledger (state-file/report only today); clearing DictWinTracker windows on SIGHUP dictionary rotation (doc-disclosed caveat today); an SSE multipart variant of the ledger churn e2e; access-point copy sources bypass source-side s3:GetObject policy enforcement at the gateway (no AP-ARN→bucket mapping; the backend enforces); s4-codec-wasm native cpu-zstd-dict decode.

Stability policy in practice

  • Adding a new codec / SSE envelope / sidecar version / CLI subcommand / lib function is additive = ships in a minor release. The v0.9 verify-sidecar subcommand + the v3 sidecar variant + the S4E6 chunked envelope are examples of minor-release additions.
  • Changing the wire format of an existing magic (e.g. shrinking S4F2's header) is breaking = ships in a major release.
  • Removing a CLI subcommand or a pub function is breaking = ships in a major release after a deprecation cycle.
  • Default value drifts for runtime tunables — not breaking per the carve-out above, but always called out in CHANGELOG ### Changed.

The audit trail for what counts as breaking lives in CHANGELOG.md per the Keep a Changelog format S4 uses end-to-end. Migration recipes for any future v2.0 will live in docs/migration/<from>-to-<to>.md; no such file exists today because no breaking change is on the v1.x roadmap.

v0.x → v1.0 source compatibility note

The v1.0 cut adds #[non_exhaustive] to every public enum we consider growable as a forward-compat measure. The annotation is a Rust-source-level forward-compat tool — it works the same way whether the containing module is frozen by name in the table above or not. (AWS-spec-bound enums — e.g. IncludedVersions, LifecycleStatus, LockMode, Effect, ReplicationStatus, VersioningState — are intentionally NOT annotated because their wire-side values are pinned by the AWS S3 spec and we do not expect to grow them; if AWS ever does, that becomes a SemVer-major event for us anyway.) The complete list of annotated enums (34 total across s4-codec + s4-server + s4-config): CodecKind, CodecError, IndexError, FrameError, CompareOp, GpuSelectError (s4-codec); RepairError, SidecarStatus, OrphanReason, DeletePolicy, SseError, SseSource<'a>, BlobError, KmsError, SigV4aError, SelectError, SelectInputFormat, SelectOutputFormat, CorsValidationError, AuditKeyError, VerifyError, TagError, MfaError, InventoryFormat, RunError, EventType, Destination, MultipartSseMode, LifecycleAction, PrincipalSet, ResourceArn, PolicyParseError, SigV4aGateError (s4-server); CompressionMode (s4-config). Full diff in commits ce30dde + db06912 + the round-6 wave that closed the s4-config gap. This is a SemVer-compatible change for the wire format and for additive runtime behavior, but it IS a source-level break for downstream code that did exhaustive match against these enums without a _ => catch-all arm. The fix on the consumer side is mechanical: add a _ => arm to every affected match. We make this an explicit caveat rather than the usual additive-minor treatment because it lands at the v0.x→v1.0 cut; we do not consider this a v2.0 trigger because the alternative (shipping these enums without #[non_exhaustive] and locking ourselves into never adding a variant within v1.x) would be the worse contract. From v1.0 onward, adding a variant to any of these enums is purely additive (the non_exhaustive attribute is the contract that makes it so).