youndie@kotlin.website:~/src$ cat bochka/README.md

bochka

jvm · s3 · zero-copy · testing
An S3-compatible object store for one machine — and the same server runs inside a test, where the JVM otherwise offers a mock. All four body framings, versioning, object lock and lifecycle rules that are applied rather than stored; 426 of 744 ceph/s3-tests with a classified reason on every remaining failure.

Why it exists

Two gaps, and the same server fills both.

On the JVM, "an S3 endpoint you can start in a test" is a mock, and a mock answers what it was told to answer — it will accept a signature it never verified and a body framed in a way no real client sends. The alternatives are a container of MinIO, which is a distributed store running in a mode it was not built for, or the real thing over the network, which is not a test.

The other gap is the small deployment: one machine, one disk, objects that have to survive a kill. Everything in that space is either a cluster with a coordinator or a toy that loses data on a power cut.

What you get

  • All four ways a client can frame a body — signed payload, UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD and STREAMING-UNSIGNED-PAYLOAD-TRAILER — because the default for aws s3 cp is the third one, and a server that reads Content-Length bytes off a socket does not work with it at all.
  • SigV4 verified against all 34 official AWS vectors, run in the verifying direction, plus presigned URLs and browser POST form uploads with both signature versions.
  • Objects, properly: Range, metadata, checksums in both forms, server-side copy, conditional reads and writes, GetObjectAttributes, partNumber on a read, multipart upload with UploadPartCopy.
  • Versioning, object lock and lifecycle rules — and lifecycle rules that are applied: objects and noncurrent versions expire, orphaned delete markers and abandoned uploads go, and x-amz-expiration says when.
  • Durability that was tested by killing it. The write order's worst outcome is an orphaned file, never a key pointing at nothing; the index is a log with a checksum per record; crash tests SIGKILL a child JVM mid-write and require everything the log admitted to to read back.
  • A GET is transferTo from the file straight into the socket — measured at 7.6–8.0× less processor per byte than reading into the heap, across a real network card.
  • A published ceiling rather than a surprise. Every key lives in memory, so the number of versions is bounded by the heap: 399 215 on the shipped 512 MiB profile, printed on the first line of the log. Going over it is a refusal to start, not a slide into swap.

What it is not

One process, one node, no replication. No ACLs, bucket policies or IAM — the access keys are a static list, narrowed to a mode and a set of buckets. No TLS inside the process: terminating it here would wrap the socket and silently remove the zero-copy read path. No storage classes, so a lifecycle rule carrying a Transition is refused by name rather than stored and never acted on.

The number

426 of 744 in ceph/s3-tests, revision 5522d1c. The other 318 carry a classified reason each — 316 deliberate scope decisions, two waiting on a source rather than on work, zero known defects — and a failure matching no rule is reported by name as unclassified rather than folded into a category. That count is zero, and the reasons behind it are re-read when a feature lands, because a rule keeps matching long after its reason stops being true.

Run it

docker run -d --name bochka \
  -u 1000:1000 \
  -v /srv/bochka:/var/lib/bochka \
  -p 127.0.0.1:9000:9000 \
  -e BOCHKA_KEYS='youraccesskey:yoursecretkey' \
  ghcr.io/youndie/bochka:latest

Start it inside a test

repositories { maven("https://reposilite.kotlin.website/snapshots") }
dependencies { testImplementation("io.github.youndie.bochka:bochka-embedded:0.2.0") }
class ReportsTest {
    companion object {
        @JvmField
        @RegisterExtension
        val bochka = BochkaExtension()
    }

    @Test
    fun `uploads the report`() {
        // bochka.endpoint, bochka.accessKeyId, bochka.secretKey — point any SDK at it
    }
}

Same server as the image: same signature verification, same body framings, same storage. It also does the one thing a real store cannot and a test double should — failNext(503) makes the next request fail, so client code whose retries were never exercised gets exercised.