OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

swift — Interactions

Interactions — swift — branch stable/2026.2

Branch stable/2026.2 @ a1bdaeba21bd — repository https://opendev.org/openstack/swift — analysed on 2026-10-29

Swift has no SQL server, no message bus and no shared cache of record. Its processes cooperate through four channels, and each is a compatibility contract for a node written in another language that must coexist with Python nodes in the same cluster:

  1. Rings (*.ring.gz files): the placement function every process evaluates locally (§ 1).
  2. Internal HTTP between proxies, storage servers and daemons: object/container/account verbs with X-Backend-* headers, REPLICATE (JSON RPC for DBs, pickle for objects) and SSYNC (§ 2).
  3. rsync daemons on every storage node, used by the replicators (§ 3).
  4. The disk itself: object files with pickled xattrs, hashes.pkl, async pendings, SQLite listing DBs with .pending files (§ 4, § 5).

Plus optional external services: memcached (§ 6), Keystone, Barbican/KMIP, statsd, syslog (§ 7). Machine-readable versions: _rewrite/vectors-*_stable-2026.2.json, _rewrite/schema-sqlite-*_stable-2026.2.sql and the sample rings in _rewrite/.

1. Rings

1.1 Files and loading

RingFileUsed by
account/etc/swift/account.ring.gzproxy, container servers (account updates), account daemons
container/etc/swift/container.ring.gzproxy, object servers/updater (container updates), container daemons, reconciler
object, policy N/etc/swift/object.ring.gz (N = 0), object-N.ring.gzproxy, object daemons

Processes re-read the file when its mtime changes, at most every reload_time (15 s) (swift/common/ring/ring.py:33, swift/common/ring/ring.py:478). A new ring that fails validation is ignored at runtime and kept at startup as an error (swift/common/ring/ring.py:344). The .builder files (pickled, see § 4.7) are operator inputs only; servers never read them.

1.2 Placement function

key       = md5(HASH_PATH_PREFIX + "/" + "/".join([account, container?, object?]) + HASH_PATH_SUFFIX)   # UTF-8 bytes
partition = big_endian_uint32(key[0:4]) >> part_shift          # part_shift = 32 - part_power
  • hash_path (swift/common/utils/__init__.py:1193); prefix and suffix come from /etc/swift/swift.conf [swift-hash] swift_hash_path_prefix / swift_hash_path_suffix, encoded latin-1 (swift/common/utils/__init__.py:373). Changing them makes every object unreachable: they are cluster secrets that a Go node must read from the same file.
  • get_part (swift/common/ring/ring.py:498).
  • Primaries (swift/common/ring/ring.py:487): for each row r of replica2part2dev_id (in order) whose length is > partition, take devs[row[partition]], skipping a device already taken; each node dict gets index = position in that list. With fractional replica counts the last row is shorter (swift/common/ring/ring.py:50).
  • Handoffs get_more_nodes (swift/common/ring/ring.py:561): deterministic walk that a Go proxy must reproduce exactly, otherwise Python and Go proxies send handoff writes to different nodes:
    1. start = big_endian_uint32(md5(str(part))[0:4]) >> part_shift; inc = max(1, partition_count // 65536) (swift/common/ring/ring.py:586).
    2. Iterate handoff_part over range(start, parts, inc) then range(inc - ((parts - start) % inc), start, inc); for each, iterate the replica rows in order.
    3. Four passes over that sequence: first yield devices in a region not yet used, then in a new (region, zone), then a new (region, zone, ip), then any unused device; each pass stops as soon as every region / zone / ip / assigned device has been used (swift/common/ring/ring.py:592–swift/common/ring/ring.py:672). Only devices that have partitions assigned count (swift/common/ring/ring.py:364).
    4. Each yielded node gets handoff_index (0, 1, 2…).
  • Vectors: _rewrite/vectors-ring_stable-2026.2.json gives, for 4 rings (3 replicas v1 and v2, 1.5 replicas, EC 4+2) and 32 names, the hash, partition, primaries and first 6 handoffs computed by this code.

1.3 Ring file format

Both versions are a gzip stream (mtime fixed at 1300507380 so equal rings are byte-identical, swift/common/ring/ring.py:282), written atomically (temp file + fsync + rename, swift/common/ring/io.py:400). The first 6 uncompressed bytes are R1NG + big-endian uint16 version (swift/common/ring/io.py:270). Version 1 is the default writer format (swift/common/ring/ring.py:47); readers accept 1 and 2.

Version 1 (swift/common/ring/ring.py:117, writer swift/common/ring/ring.py:229):

Offset (uncompressed)Content
0R1NG\x00\x01
6uint32 big-endian L
10L bytes of ASCII JSON, keys sorted: devs, part_shift, replica_count (integer number of rows), byteorder ("little"/"big"), optional version, next_part_power
10+Lreplica_count rows, each 2 × partition_count bytes: unsigned 16-bit device ids in the byte order named in the JSON (byteswap if it differs from the host)

Only 2-byte device ids are allowed in v1 (swift/common/ring/ring.py:230). The last row is full length even for fractional replica counts in v1 [inferred from the writer: len(row) is not written; the builder pads with the "none" id].

Version 2 (swift/common/ring/ring.py:153, writer swift/common/ring/ring.py:257, container format swift/common/ring/io.py:484):

  • The stream is made of sections, each flushed with Z_FULL_FLUSH so that a reader can seek to its compressed offset and start a new raw-deflate decompressor there (swift/common/ring/io.py:442).
  • Each section is a length-prefixed blob: uint64 big-endian length + data (swift/common/ring/io.py:36, swift/common/ring/io.py:591).
  • Sections, in order: swift/ring/metadata (JSON: part_shift, dev_id_bytes, replica_count as a float, version, optional next_part_power), swift/ring/devices (JSON list), swift/ring/assignments (uint64 total byte length, then the rows as unsigned 2- or 4-byte ids in network order; the last row may be shorter) (swift/common/ring/io.py:613).
  • The index follows: a JSON blob {section: [compressed_start, uncompressed_start, compressed_end, uncompressed_end, "sha256", hexdigest]} (swift/common/ring/io.py:627). The digest covers the 8-byte length prefix and the data (swift/common/ring/io.py:365).
  • The tail is written uncompressed (level 0) so that its size is fixed: two uint64 (uncompressed and compressed start of the index), each followed by a flush, then the gzip trailer. A reader seeks to -31 from the end and reads the compressed index offset (swift/common/ring/io.py:301).
  • Device id 2^(8·dev_id_bytes) − 1 (0xFFFF) means "no device" (swift/common/ring/utils.py:41).

Device dict (JSON, null entries for removed ids): id, region, zone, ip, port, replication_ip, replication_port (defaulted to ip/port, swift/common/ring/ring.py:58), device, weight, meta, and builder fields (parts, parts_wanted are not saved in the ring).

1.4 Partition power increase

next_part_power in the ring triggers relinking: object servers write each new file twice (hard link in the partition of the next power, X-Backend-Next-Part-Power) (swift/obj/server.py:846, swift/obj/diskfile.py:502), and swift-object-relinker relinks/cleans existing files. A Go object server must honour next_part_power or a power increase in progress corrupts placement.

2. Internal HTTP protocol

All internal requests use HTTP/1.1 over TCP (swift/common/bufferedhttp.py), path /<device>/<partition>/<quoted account>[/<container>[/<object>]], built by http_connect (swift/common/bufferedhttp.py). There is no authentication between nodes: the storage network must be isolated (a Kubernetes NetworkPolicy in OpenKeel). Every backend request carries X-Timestamp (writes), X-Trans-Id, User-Agent: proxy-server <pid> / object-server <pid>… and X-Backend-Storage-Policy-Index for objects.

2.1 Proxy → container server (container PUT/DELETE)

Request headers: X-Timestamp, X-Account-Host (ip:port), X-Account-Partition, X-Account-Device, X-Backend-Storage-Policy-Index (PUT), metadata headers. The container server then updates the account server (§ 2.2).

2.2 Container server → account server

PUT /<dev>/<part>/<account>/<container> with X-Put-Timestamp, X-Delete-Timestamp, X-Object-Count, X-Bytes-Used, X-Backend-Storage-Policy-Index, X-Trans-Id, X-Account-Override-Deleted (swift/container/server.py:209). The container updater sends the same request periodically with fresh counts (swift/container/updater.py:40).

2.3 Proxy → object server (object PUT)

Headers built by _backend_requests (swift/proxy/controllers/obj.py:494):

HeaderMeaning
X-Container-Host, X-Container-Device (comma lists), X-Container-Partitioncontainer replicas this object server must update; spread so that every container replica gets num_container_updates updates (swift/proxy/controllers/obj.py:90)
X-Container-Root-Db-State, X-Backend-Quoted-Container-Pathsharded container: update the shard instead of the root (swift/proxy/controllers/obj.py:513)
X-Delete-At-Host/-Device/-Partition/-Container.expiring_objects queue container to update when X-Delete-At is set
X-Backend-Storage-Policy-Indexpolicy (decides objects-N directory)
X-Backend-Obj-Content-Length, X-Backend-Obj-Multipart-Mime-Boundary, X-Backend-Obj-Metadata-Footer: yes, X-Backend-Obj-Multiphase-Commit: yesEC (and encryption footers) MIME PUT (swift/proxy/controllers/obj.py:2095)
Expect: 100-continue, Transfer-Encoding: chunkedalways for PUT; the object server checks the disk before answering 100

Replicated PUT: plain chunked body (swift/proxy/controllers/obj.py:1078).

MIME PUT (EC, and replicated PUT when a middleware sets footers, swift/proxy/controllers/obj.py:1089). Protocol between proxy and object server (swift/proxy/controllers/obj.py:1976, swift/obj/server.py:893):

  1. The object server answers the first 100 Continue with X-Obj-Metadata-Footer: yes and X-Obj-Multiphase-Commit: yes (swift/obj/server.py:897). A server that does not, makes the proxy fail with FooterNotSupported / MultiphasePUTNotSupported (swift/proxy/controllers/obj.py:2115).
  2. Body (chunked): --<boundary>\r\nX-Document: object body\r\n\r\n<fragment archive bytes> (swift/proxy/controllers/obj.py:2004), then \r\n--<boundary>\r\nX-Document: object metadata\r\nContent-MD5: <md5 of JSON>\r\n\r\n<JSON footer>\r\n--<boundary>\r\n (swift/proxy/controllers/obj.py:2007).
  3. EC footer JSON keys (swift/proxy/controllers/obj.py:2200): X-Object-Sysmeta-EC-Etag (MD5 of the client object), X-Object-Sysmeta-EC-Content-Length, X-Object-Sysmeta-Ec-Frag-Index, X-Object-Sysmeta-EC-Scheme, X-Object-Sysmeta-EC-Segment-Size, X-Backend-Container-Update-Override-Etag/-Size, Etag (MD5 of the fragment archive). Encryption adds X-Object-Sysmeta-Crypto-* footers.
  4. The object server writes the .data (non-durable: <ts>#<frag>.data) and sends a second 100 Continue (swift/obj/server.py:1029).
  5. When a quorum of servers has sent the second 100, the proxy sends X-Document: put commit\r\n\r\nput_commit_confirmation\r\n--<boundary>-- (swift/proxy/controllers/obj.py:2044). The object server renames the file to <ts>#<frag>#d.data (durable) (swift/obj/diskfile.py:3391) and answers 201.
  6. Errors: missing boundary 400, footer MD5 mismatch 422, no commit document 400 or 500 (swift/obj/server.py:555, swift/obj/server.py:583).

The container update then uses X-Backend-Container-Update-Override-* (or X-Object-Sysmeta-Container-Update-Override-*) instead of the fragment's own size and ETag (swift/obj/server.py:606).

2.4 Object server → container server (listing update)

PUT|DELETE /<dev>/<part>/<account>/<container>/<object> (or to X-Backend-Quoted-Container-Path for a shard) with X-Size, X-Content-Type, X-Etag, X-Timestamp, optional X-Content-Type-Timestamp, X-Meta-Timestamp, X-Backend-Storage-Policy-Index, Referer, X-Trans-Id (swift/obj/server.py:363, swift/obj/server.py:1053). Sent concurrently to each container host given by the proxy; the object server waits container_update_timeout (1 s default). On failure or timeout the update is written as an async pending (§ 4.4). A 301 from a sharded root carries Location / X-Backend-Location-Is-Quoted and the update is redirected (swift/obj/server.py:330). The container server merges the row with update_new_item_from_existing (§ 5.3) (swift/container/server.py:496).

2.5 Object GET/HEAD response headers (backend)

X-Timestamp, X-Backend-Timestamp (newest file, also on 404 for tombstones), X-Backend-Data-Timestamp, X-Backend-Durable-Timestamp and X-Backend-Fragments (EC: JSON of other fragment indexes this node holds) (swift/obj/server.py:1191); all metadata from the xattrs. The proxy sends X-Backend-Fragment-Preferences (JSON) to choose fragments of one timestamp (swift/proxy/controllers/obj.py:2536) and X-Backend-Etag-Is-At: X-Object-Sysmeta-Ec-Etag so that conditional requests use the client ETag (swift/proxy/controllers/obj.py:2949).

2.6 Object REPLICATE

REPLICATE /<dev>/<part>[/<suffix>-<suffix>…] with X-Backend-Storage-Policy-Index (swift/obj/server.py:1387):

  • without suffixes: return the partition's suffix hashes (recomputing invalid ones);
  • with suffixes: recompute those suffixes first (skip_rehash false only for the others).

Response body: pickle protocol 2 of {suffix: md5hex} (replication) or {suffix: {frag_index_or_None: md5hex}} (EC) (swift/obj/server.py:1411), read with the restricted unpickler (swift/obj/replicator.py:728, swift/obj/reconstructor.py:940). 507 when the device is unavailable. A Go node must both produce this pickle (Python peers unpickle it) and parse it (§ 4.8).

2.7 SSYNC

Used by the reconstructor (always) and by the replicator with sync_method = ssync. One HTTP request, chunked in both directions (swift/obj/ssync_sender.py:240, swift/obj/ssync_receiver.py:275):

C: SSYNC /<dev>/<part> HTTP/1.1
   Transfer-Encoding: chunked
   X-Backend-Storage-Policy-Index: <n>
   X-Backend-Ssync-Frag-Index: <i>        (EC; X-Backend-Ssync-Node-Index sent too, for 2.4–2.20)
S: 200 OK, X-Backend-Accept-No-Commit: True, chunked
C: :MISSING_CHECK: START\r\n
   <hash> <ts_data>[ m:<hex delta>[__<hex offset>][,t:<hex delta>…][,durable:False]]\r\n   (one per local object)
   :MISSING_CHECK: END\r\n
S: :MISSING_CHECK: START\r\n
   <hash> <d|m|dm>\r\n                  (wanted parts)
   :MISSING_CHECK: END\r\n
C: :UPDATES: START\r\n
   chunk: "PUT /<a>/<c>/<o>\r\n<Header>: <value>\r\n…\r\n\r\n" then the body as chunks
   chunk: "DELETE /<a>/<c>/<o>\r\nX-Timestamp: <ts>\r\n\r\n"
   chunk: "POST /<a>/<c>/<o>\r\n<metadata headers>\r\n\r\n"
   :UPDATES: END\r\n
S: :UPDATES: START\r\n :UPDATES: END\r\n   (or ":ERROR: <status> '<message>'")
  • Line formats: encode_missing / decode_missing (swift/obj/ssync_sender.py:26, swift/obj/ssync_receiver.py:31); deltas are in units of 10 µs (Timestamp.raw). encode_wanted / decode_wanted (swift/obj/ssync_receiver.py:70, swift/obj/ssync_sender.py:60).
  • Subrequests: headers sorted, then the body as hex-length chunks (swift/obj/ssync_sender.py:466). PUT sends all data-file metadata except name; X-Backend-No-Commit: True for non-durable EC fragments (swift/obj/ssync_sender.py:501).
  • The receiver runs each subrequest against its own object server with X-Backend-Replication: True and X-Backend-Replication-Headers (which headers to persist as-is) (swift/obj/ssync_receiver.py:612). It takes the partition's replication lock (replication_lock_timeout) and answers :ERROR: lines on failure.
  • Vectors: _rewrite/vectors-replication_stable-2026.2.json (ssync_missing_check, ssync_wanted).

2.8 Account/container REPLICATE (DB replication RPC)

REPLICATE /<dev>/<part>/<hash> with body application/json = a JSON list [op, args…] (swift/common/db_replicator.py:162), dispatched by ReplicatorRpc.dispatch (swift/common/db_replicator.py:960):

opargsResponse
syncmax_row, hash, id, created_at, put_timestamp, delete_timestamp, metadata(JSON string) (swift/common/db_replicator.py:518)200 JSON of the local replication info + point (last sync point from the caller); 404 if the DB does not exist (caller then rsyncs it) (swift/common/db_replicator.py:1019)
merge_items[rows], local_id — rows as returned by get_items_since (SELECT * … ROWID > ?, dicts with ROWID) (swift/common/db.py:693)202 (swift/common/db_replicator.py:1081)
merge_syncs[{remote_id, sync_point}]202
complete_rsynctmp_name[, db_basename]: install <dev>/tmp/<tmp_name> as the DB204 / 404 (swift/common/db_replicator.py:1085)
rsync_then_mergetmp_name, db_basename: merge the existing DB into the rsynced one, then replace204 / 404 (swift/common/db_replicator.py:1106)
get_shard_ranges, merge_shard_rangescontainer only (swift/container/replicator.py:439)JSON / 202

The sender chooses between "in sync" (same hash or sync point ≥ max_row), row-by-row usync (per_diff 1000 rows × max_diffs 100) and full rsync + rsync_then_merge when the remote has less than half the rows (swift/common/db_replicator.py:572). A 507 response raises DriveNotMounted (swift/common/db_replicator.py:558).

2.9 Other internal clients

direct_client (daemons, CLI tools: direct GET/HEAD/PUT/DELETE on nodes, swift/common/direct_client.py) and InternalClient (a proxy pipeline in-process, used by the expirer, reconciler, container-sync: etc/internal-client.conf-sample:30). The expirer, reconciler and sharder talk to .expiring_objects, .misplaced_objects and .shards_<account> hidden accounts through it.

3. rsync

Every storage node runs rsyncd with modules account, container, object (optionally per device object_<dev>), configured by rsync_module = {replication_ip}::object (swift/obj/replicator.py:172, swift/common/db_replicator.py:282).

  • Objects: rsync --recursive --whole-file --human-readable --xattrs --itemize-changes --ignore-existing --timeout=… --contimeout=… --bwlimit=… --exclude=.*.<6 chars> <suffix dirs…> <module>/<device>/objects[-N]/<partition> (swift/obj/replicator.py:434), then REPLICATE /<dev>/<part>/<suffixes> so that the receiver recomputes those suffix hashes (swift/obj/replicator.py:480). xattrs travel with the files: the receiver's filesystem must accept user.* xattrs of arbitrary size.
  • DBs: rsync --quiet --no-motd --whole-file <db> <module>/<device>/tmp/<local_id>, a second pass without --whole-file if the DB changed, then complete_rsync or rsync_then_merge (swift/common/db_replicator.py:379).

A Go storage node coexisting with Python replicators must therefore run an rsync daemon exposing the same module layout (or the cluster must switch objects to sync_method = ssync; DB replication has no ssync alternative).

4. Object storage on disk

4.1 Layout

/srv/node/<device>/                                   (devices option; mount_check verifies a mount point)
  objects[-N]/<partition>/<suffix>/<hash>/<files>     N = policy index (none for 0), suffix = hash[-3:]
  objects[-N]/<partition>/hashes.pkl                  cached suffix hashes
  objects[-N]/<partition>/hashes.invalid              suffixes to rehash, one per line
  objects[-N]/<partition>/.lock                       partition lock (flock)
  async_pending[-N]/<hash[-3:]>/<hash>-<timestamp>    pending container updates (pickle)
  tmp[-N]/                                            temporary files (same filesystem, for rename)
  quarantined/objects[-N]/<hash>[-<uuid>]/            quarantined hash directories
  accounts/<part>/<suffix>/<hash>/<hash>.db[.pending]
  containers/<part>/<suffix>/<hash>/<hash>[_<epoch>].db[.pending]

Sources: swift/obj/diskfile.py:84–swift/obj/diskfile.py:98, swift/obj/diskfile.py:556, swift/common/utils/__init__.py:1181, swift/obj/diskfile.py:1496, swift/obj/diskfile.py:364. <hash> is the hex hash_path(account, container, object).

4.2 File names

NameMeaningSource
<ts>.dataobject data + metadata (replication)swift/obj/diskfile.py:822
<ts>#<frag>.data, <ts>#<frag>#d.dataEC fragment archive, non-durable / durableswift/obj/diskfile.py:3620
<ts>.durablelegacy EC durable marker (still read)swift/obj/diskfile.py:3706
<ts>.meta, <ts>-<hex>.meta / <ts>+<hex>.metaPOST metadata; the second form also carries the content-type timestamp as a delta (encode_timestamps(t_meta, t_ctype, explicit=True))swift/obj/diskfile.py:836
<ts>.tstombstone—

<ts> is Timestamp.internal: %016.05f of the Unix time, plus _%016x when the offset is non-zero (swift/common/utils/timestamp.py:24, swift/common/utils/timestamp.py:454). Timestamps compare by (time in 10 µs units, offset). Vectors: _rewrite/vectors-timestamp_stable-2026.2.json.

Which files are valid (get_ondisk_files, swift/obj/diskfile.py:971): sort each extension newest first; the newest .ts makes everything not newer than it obsolete; keep the newest .meta and, if an older .meta has a newer content-type timestamp, that one too; replication keeps the newest .data (older ones obsolete, swift/obj/diskfile.py:3182); EC keeps fragment sets by timestamp, the newest durable set is chosen, non-durable newer sets are kept until reclaim_age/commit_window (swift/obj/diskfile.py:3706). Files that do not parse are "unexpected" and left alone (rsync temp files are recognised, swift/obj/diskfile.py:98). cleanup_ondisk_files deletes obsolete files, a .ts older than reclaim_age, and the directory when empty (swift/obj/diskfile.py:1120). Vectors: _rewrite/vectors-ondisk_stable-2026.2.json.

4.3 Writing an object

DiskFileWriter (swift/obj/diskfile.py:1793): create a temp file in tmp[-N] (O_TMPFILE + linkat when supported), fallocate the size plus fallocate_reserve checks, write, fdatasync every bytes_per_sync, write xattrs, fsync, rename/linkat into the hash directory, fsync the directory, then invalidate_hash(suffix) and cleanup of obsolete files (swift/obj/diskfile.py:1958). EC commit renames #<frag>.data to #<frag>#d.data (swift/obj/diskfile.py:3318).

4.4 Async pendings

When the container update fails, pickle_async_update writes async_pending[-N]/<hash[-3:]>/<hash>-<ts> with a protocol-2 pickle of {"op", "account", "container", "obj", "headers", "db_state", ["container_path"]} (swift/obj/diskfile.py:1477, swift/obj/server.py:349). The object updater replays and deletes them (05). Vector: _rewrite/vectors-pickle_stable-2026.2.json → pending_files.async_pending.

4.5 Object metadata in xattrs

ItemFormatSource
keysuser.swift.metadata, user.swift.metadata1, user.swift.metadata2… each ≤ 65536 bytes; the pickle is the concatenationswift/obj/diskfile.py:272, swift/obj/diskfile.py:201
checksumuser.swift.metadata_checksum = ASCII hex MD5 of the whole pickle; checked on read (mismatch → quarantine); may be absent on old filesswift/obj/diskfile.py:218
valuepickle.dumps(dict, protocol=2) of a flat dict of strings, keys and values UTF-8 encoded bytes (surrogateescape)swift/obj/diskfile.py:150, swift/obj/diskfile.py:279
decodingrestricted unpickler, encoding='bytes'; files written by Python 2 (no _codecs\nencode in the first 32 bytes) have values decoded as latin-1 then re-encoded, except nameswift/obj/diskfile.py:243, swift/obj/diskfile.py:164

Keys in the dict: name (/<account>/<container>/<object>), X-Timestamp, Content-Type, Content-Length, ETag, X-Object-Meta-*, X-Object-Sysmeta-*, X-Object-Transient-Sysmeta-*, X-Delete-At, Content-Disposition, Content-Encoding, X-Static-Large-Object, X-Object-Manifest… (swift/obj/server.py:965). .meta files hold only X-Timestamp, user/transient metadata and optionally Content-Type/Content-Type-Timestamp. How to read and write these pickles without Python: § 4.8.

4.6 hashes.pkl and hashes.invalid

  • hashes.pkl: pickle protocol 2 of {suffix: hash_or_None, "valid": bool, "updated": float} (swift/obj/diskfile.py:388, swift/obj/diskfile.py:422). None means "must be recomputed". Replication hash = MD5 hex string; EC hash = dict {frag_index (int) or None: md5hex}.
  • hashes.invalid: suffixes appended (<suffix>\n) under the partition lock by every write (swift/obj/diskfile.py:477); consolidate_hashes folds them into hashes.pkl as None and truncates the file (swift/obj/diskfile.py:436).
  • Suffix hash (replication) = MD5 over, for each hash directory in sorted order, the strings <ts>.<ext> of the chosen .ts, .meta, .data files and <ctype ts>_ctype (swift/obj/diskfile.py:1190, swift/obj/diskfile.py:3210). EC: one MD5 per fragment index of the <ts> of each fragment, and the None bucket gets <ts>.durable of the durable set, plus meta/ts/ctype (swift/obj/diskfile.py:3898). Emptied suffix directories are removed and dropped from the dict (swift/obj/diskfile.py:1282).
  • Writers re-read hashes.pkl under the lock and write only if it did not change, else recompute (swift/obj/diskfile.py:1393).
  • These values are compared across nodes by REPLICATE: a Go node that hashes differently makes every suffix look out of sync and triggers endless rsync. Vectors: _rewrite/vectors-ondisk_stable-2026.2.json → suffix_hashes.

4.7 Other pickles

FileContentSource
<name>.builderpickle.dump(builder.to_dict(), protocol=2), loaded with unrestricted pickle.loadswift/common/ring/builder.py:1834, swift/common/ring/builder.py:1794
account/container .pending: + base64(pickle protocol 2 of a tuple) per record (§ 5.2)swift/common/db.py:814
async_pending§ 4.4swift/obj/diskfile.py:1496
REPLICATE body§ 2.6swift/obj/server.py:1411

4.8 Reading Swift pickles safely without Python

Swift already refuses arbitrary code: RestrictedUnpickler.find_class only resolves _codecs.encode, copy_reg/copyreg._reconstructor, swift.common.header_key_dict.HeaderKeyDict, builtins/__builtin__.dict and .bytes, and raises for anything else (swift/common/utils/pickle.py:49, swift/common/utils/pickle.py:63). A Go (or any) implementation should be stricter: a small pickle decoder (not a VM) that accepts only the opcodes and globals below and maps them to native values. No pickle is ever executed.

Opcodes observed in every on-disk and wire pickle written by Swift on Python 3 (_rewrite/vectors-pickle_stable-2026.2.json lists them per vector):

ProducerOpcodesGlobals
xattr metadata (py3)PROTO 2, EMPTY_DICT, MARK, BINUNICODE, GLOBAL, BINPUT, BINGET, TUPLE2, REDUCE, SETITEMS, STOP_codecs encode — bytes are encoded as _codecs.encode(<latin-1 text>, "latin1")
xattr metadata (written by py2)PROTO, EMPTY_DICT, MARK, SHORT_BINSTRING, BINSTRING, BINUNICODE, BINPUT, SETITEMS, STOPnone
hashes.pklPROTO, EMPTY_DICT, MARK, BINUNICODE, NONE, NEWTRUE/NEWFALSE, BINFLOAT, BININT1/BININT, BINPUT, SETITEMS, STOPnone
REPLICATE bodyidem hashes.pkl without valid/updatednone
.pending recordsPROTO, MARK, BINUNICODE, BININT1/BININT/LONG1, NONE, BINPUT, TUPLE, STOPnone
async pendingPROTO, EMPTY_DICT, MARK, BINUNICODE, BININT1, BINPUT, SETITEMS, STOPnone (headers may be a HeaderKeyDict when written by old versions: copyreg._reconstructor + swift.common.header_key_dict HeaderKeyDict [inferred from the allow-list])

Decoding rules:

  1. Reject unknown opcodes and every GLOBAL/STACK_GLOBAL other than the allow-list; treat _codecs.encode(s, "latin1") + REDUCE as "bytes = latin-1 code points of s"; treat HeaderKeyDict reconstruction as a plain string map.
  2. Keep a memo for BINPUT/LONG_BINPUT/BINGET/LONG_BINGET; cap the input size (metadata is bounded by MAX_META_OVERALL_SIZE plus system keys; reject > 1 MiB) and the nesting depth.
  3. For metadata: decode keys and values as UTF-8 with surrogateescape semantics (keep invalid bytes); if the pickle has no _codecs\nencode in its first 32 bytes it was written by Python 2: decode non-name values as latin-1 then UTF-8 (swift/obj/diskfile.py:171).
  4. Verify user.swift.metadata_checksum before decoding when present.
  5. To write, emit exactly the Python 3 form (protocol 2, _codecs.encode for bytes) so that Python nodes, swift-object-info and the auditor read it; the vectors give byte-exact expected outputs for given dicts. Python's pickle does not produce a canonical form across versions in general, but protocol 2 output of a flat str→bytes dict is stable [inferred; checked on CPython 3.13 only].
  6. The .builder files are the exception: Swift itself loads them with an unrestricted unpickler. Keep swift-ring-builder in Python (operator tool) or convert builders once to JSON; never load them in a server.

5. SQLite listing databases

5.1 Schemas

Created by the brokers on first PUT (swift/account/backend.py:59, swift/container/backend.py:535), not by migrations. The exact DDL produced by this branch is in _rewrite/schema-sqlite-account_stable-2026.2.sql and _rewrite/schema-sqlite-container_stable-2026.2.sql.

DBTablesNotes
bothoutgoing_sync, incoming_sync (remote_id UNIQUE, sync_point, updated_at) + triggersreplication high-water marks (swift/common/db.py:389)
accountcontainer (ROWID, name, put_timestamp, delete_timestamp, object_count, bytes_used, deleted, storage_policy_index), account_stat (one row: account, created_at, put/delete timestamps, container/object counts, bytes, hash, id, status, status_changed_at, metadata), policy_stattriggers keep account_stat counts and hash (swift/account/backend.py:73, swift/account/backend.py:125, swift/account/backend.py:158)
containerobject (ROWID, name, created_at, size, content_type, etag, deleted, storage_policy_index), container_info + view container_stat (INSTEAD OF UPDATE trigger), policy_stat, shard_rangeobject rows are never updated (trigger RAISE(FAIL…)): DELETE + INSERT (swift/container/backend.py:554, swift/container/backend.py:117, swift/container/backend.py:634)
  • **chexor** is a SQL function the triggers call: hash = hex(int(old,16) XOR int(md5("<name>-<created_at>"),16)), 32 hex digits (swift/common/db.py:176). Every connection must register it (swift/common/db.py:224); a Go node must register the same function in its SQLite driver, or every insert fails. The hash is compared across replicas by the sync RPC. Vectors: _rewrite/vectors-replication_stable-2026.2.json → chexor.
  • Connection settings: PRAGMA synchronous = NORMAL, count_changes = OFF, temp_store = MEMORY, journal_mode = DELETE (swift/common/db.py:220). Rollback journal, not WAL: rsync of a DB relies on it.
  • Legacy schemas are migrated lazily on use (_migrate_add_storage_policy, _migrate_add_container_sync_points, _migrate_add_shard_range_reported…) (swift/container/backend.py:1561–swift/container/backend.py:1662, swift/account/backend.py:588). A Go reader must accept DBs without these columns or run the same ALTER TABLEs.
  • created_at of an object row stores up to three timestamps: encode_timestamps(t_data, t_ctype, t_meta) = <t_data.short>[±<hex delta ctype>[±<hex delta meta>]] (swift/common/utils/timestamp.py:490).
  • content_type may carry ;swift_bytes=<n> (SLO/encryption real size), stripped from listings (swift/container/backend.py:229).
  • metadata column: JSON {name: [value, timestamp]}; updates win by timestamp; an empty value deletes (swift/common/db.py:1015).
  • Sharded containers: the root DB is renamed <hash>_<epoch>.db during sharding; get_db_state returns unsharded, sharding, sharded, collapsed (swift/container/backend.py:45, swift/container/backend.py:435). Shard ranges carry name, timestamp, lower, upper, counts, state, epoch (swift/container/backend.py:634).

5.2 .pending files

To avoid one SQLite transaction per object update, records are appended to <db>.pending as : + base64(pickle protocol 2 of a tuple) under the directory lock (swift/common/db.py:782). When the file exceeds 131072 bytes (PENDING_CAP, swift/common/db.py:52), or before any read, the broker merges it (swift/common/db.py:822).

DBTupleSource
container(name, created_at, size, content_type, etag, deleted, storage_policy_index, ctype_timestamp, meta_timestamp); older writers send 6 or 7 fieldsswift/container/backend.py:690, swift/container/backend.py:761
account(name, put_timestamp, delete_timestamp, object_count, bytes_used, deleted, storage_policy_index)swift/account/backend.py:190, swift/account/backend.py:222

Both Python and Go servers may append to the same .pending of a DB on a shared volume only if they share the lock protocol (flock on the directory's .lock, swift/common/utils/__init__.py, lock_parent_directory); in OpenKeel each PVC is owned by exactly one pod, so this only matters when replacing a Python pod by a Go pod on the same volume.

5.3 Merge rules

  • Object rows: update_new_item_from_existing merges data, content-type and metadata timestamps independently; the newest data attributes (size, etag, deleted, swift_bytes), the newest content type and the newest meta timestamp win (swift/container/backend.py:180). This makes listing updates commutative, so async pendings, replication and the updater can apply in any order.
  • Account rows: the container row with the newest put/delete timestamps wins.
  • Replication: merge_items applies the same rule and updates incoming_sync (swift/container/backend.py:1331).

6. Memcached

UsageKeysValueSource
account / container info (proxy)account/<a>, container/<a>/<c>JSON dict, TTL recheck_account_existence / recheck_container_existence (60 s)swift/proxy/controllers/base.py:614
shard rangesshard-listing-v2/<a>/<c>, shard-updating-v2/<a>/<c>JSON list of namespacesswift/proxy/controllers/base.py:641
tempauth tokens<prefix>/token/<token>, <prefix>/user/<account>:<user>JSONswift/common/middleware/tempauth.py:797
ratelimitratelimit/<a>[/<c>]counters (incr)swift/common/middleware/ratelimit.py:160
keystonemiddleware token cacheits own keyskeystonemiddleware

Wire: memcached text protocol. Keys are sent as md5hex(key) (swift/common/memcached.py:88); values JSON with flag 2, flag 1 (pickle) values are ignored on read (swift/common/memcached.py:456). Server choice: consistent hash ring of 50 points per server, md5("<server>-<i>"), next point after md5(key), up to tries servers (swift/common/memcached.py:212, swift/common/memcached.py:334). A Go proxy sharing memcached with Python proxies must use the same hashing and JSON shapes, or simply its own memcached (the cache is not a source of truth).

7. External services and other channels

SystemProtocolLibraryDirectionUsage
KeystoneREST (token validation, /v3/s3tokens)keystonemiddleware authtoken in the proxy pipeline; s3token (swift/common/middleware/s3api/s3token.py:154)proxy → Keystoneidentity for keystoneauth, S3 signatures
Barbican / KMIPREST / KMIPcastellan (kms_keymaster), PyKMIP (kmip_keymaster)proxy → KMS at startuproot secret for encryption (swift/common/middleware/crypto/keymaster.py:227)
memcachedtext protocolswift/common/memcached.pyproxy → memcached§ 6
statsdUDPswift/common/statsd_client.pyall → statsdmetrics (log_statsd_host)
syslogUDP/unix socketloggingalllogs
recon cacheJSON files /var/cache/swift/{object,container,account,drive,relinker}.reconswift/common/recon.py:17daemons write, /recon/* of storage servers readsreplication/audit/update stats (swift/common/middleware/recon.py:424)
rsyncprocess + rsync protocolsubprocessreplicators → peers§ 3
swift.confINI—all readhash prefix/suffix, storage policies, constraints
other cluster (container sync)HTTP to remote proxiesswift/container/sync.py:77container-sync daemon → remote Swift§ 05

8. Sequence: object PUT with a failed container update

sequenceDiagram
  participant C as Client
  participant P as Proxy
  participant O as Object servers (3)
  participant CS as Container servers (3)
  participant U as object-updater
  C->>P: PUT /v1/AUTH_a/c/o (token)
  P->>CS: HEAD /dev/part/AUTH_a/c (container info, if not cached)
  P->>O: PUT /dev/part/AUTH_a/c/o, X-Container-Host/Device/Partition, Expect: 100-continue
  O-->>P: 100 Continue
  P->>O: body (chunked)
  O->>O: write tmp, xattrs, fsync, rename <ts>.data, invalidate suffix
  O->>CS: PUT /dev/part/AUTH_a/c/o X-Size X-Etag X-Timestamp
  alt container update fails or times out
    O->>O: write async_pending/<sfx>/<hash>-<ts> (pickle)
  end
  O-->>P: 201
  P-->>C: 201 (quorum 2 of 3)
  U->>U: sweep async_pending (every 300 s)
  U->>CS: PUT /dev/part/AUTH_a/c/o (replayed update)
  CS-->>U: 201
  U->>U: unlink async pending

The other end-to-end flows are in 04-scenarios_stable-2026.2.md.

Source: reverse-spec-swift/03-interactions_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.