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:
- Rings (
*.ring.gzfiles): the placement function every process evaluates locally (§ 1). - Internal HTTP between proxies, storage servers and daemons: object/container/account verbs with
X-Backend-*headers,REPLICATE(JSON RPC for DBs, pickle for objects) andSSYNC(§ 2). - rsync daemons on every storage node, used by the replicators (§ 3).
- The disk itself: object files with pickled xattrs,
hashes.pkl, async pendings, SQLite listing DBs with.pendingfiles (§ 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
| Ring | File | Used by |
|---|---|---|
| account | /etc/swift/account.ring.gz | proxy, container servers (account updates), account daemons |
| container | /etc/swift/container.ring.gz | proxy, object servers/updater (container updates), container daemons, reconciler |
| object, policy N | /etc/swift/object.ring.gz (N = 0), object-N.ring.gz | proxy, 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 rowrofreplica2part2dev_id(in order) whose length is > partition, takedevs[row[partition]], skipping a device already taken; each node dict getsindex= 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:start = big_endian_uint32(md5(str(part))[0:4]) >> part_shift;inc = max(1, partition_count // 65536)(swift/common/ring/ring.py:586).- Iterate
handoff_partoverrange(start, parts, inc)thenrange(inc - ((parts - start) % inc), start, inc); for each, iterate the replica rows in order. - 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). - Each yielded node gets
handoff_index(0, 1, 2…).
- Vectors:
_rewrite/vectors-ring_stable-2026.2.jsongives, 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 |
|---|---|
| 0 | R1NG\x00\x01 |
| 6 | uint32 big-endian L |
| 10 | L bytes of ASCII JSON, keys sorted: devs, part_shift, replica_count (integer number of rows), byteorder ("little"/"big"), optional version, next_part_power |
| 10+L | replica_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_FLUSHso 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_countas a float,version, optionalnext_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-31from 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):
| Header | Meaning |
|---|---|
X-Container-Host, X-Container-Device (comma lists), X-Container-Partition | container 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-Path | sharded 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-Index | policy (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: yes | EC (and encryption footers) MIME PUT (swift/proxy/controllers/obj.py:2095) |
Expect: 100-continue, Transfer-Encoding: chunked | always 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):
- The object server answers the first
100 ContinuewithX-Obj-Metadata-Footer: yesandX-Obj-Multiphase-Commit: yes(swift/obj/server.py:897). A server that does not, makes the proxy fail withFooterNotSupported/MultiphasePUTNotSupported(swift/proxy/controllers/obj.py:2115). - 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). - 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 addsX-Object-Sysmeta-Crypto-*footers. - The object server writes the
.data(non-durable:<ts>#<frag>.data) and sends a second100 Continue(swift/obj/server.py:1029). - 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 answers201. - 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_rehashfalse 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 exceptname;X-Backend-No-Commit: Truefor non-durable EC fragments (swift/obj/ssync_sender.py:501). - The receiver runs each subrequest against its own object server with
X-Backend-Replication: TrueandX-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):
| op | args | Response |
|---|---|---|
sync | max_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_rsync | tmp_name[, db_basename]: install <dev>/tmp/<tmp_name> as the DB | 204 / 404 (swift/common/db_replicator.py:1085) |
rsync_then_merge | tmp_name, db_basename: merge the existing DB into the rsynced one, then replace | 204 / 404 (swift/common/db_replicator.py:1106) |
get_shard_ranges, merge_shard_ranges | container 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), thenREPLICATE /<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 acceptuser.*xattrs of arbitrary size. - DBs:
rsync --quiet --no-motd --whole-file <db> <module>/<device>/tmp/<local_id>, a second pass without--whole-fileif the DB changed, thencomplete_rsyncorrsync_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
| Name | Meaning | Source |
|---|---|---|
<ts>.data | object data + metadata (replication) | swift/obj/diskfile.py:822 |
<ts>#<frag>.data, <ts>#<frag>#d.data | EC fragment archive, non-durable / durable | swift/obj/diskfile.py:3620 |
<ts>.durable | legacy EC durable marker (still read) | swift/obj/diskfile.py:3706 |
<ts>.meta, <ts>-<hex>.meta / <ts>+<hex>.meta | POST 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>.ts | tombstone | — |
<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
| Item | Format | Source |
|---|---|---|
| keys | user.swift.metadata, user.swift.metadata1, user.swift.metadata2… each ≤ 65536 bytes; the pickle is the concatenation | swift/obj/diskfile.py:272, swift/obj/diskfile.py:201 |
| checksum | user.swift.metadata_checksum = ASCII hex MD5 of the whole pickle; checked on read (mismatch → quarantine); may be absent on old files | swift/obj/diskfile.py:218 |
| value | pickle.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 |
| decoding | restricted 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 name | swift/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).Nonemeans "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_hashesfolds them intohashes.pklasNoneand 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,.datafiles 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 theNonebucket gets<ts>.durableof 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.pklunder 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
| File | Content | Source |
|---|---|---|
<name>.builder | pickle.dump(builder.to_dict(), protocol=2), loaded with unrestricted pickle.load | swift/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.4 | swift/obj/diskfile.py:1496 |
| REPLICATE body | § 2.6 | swift/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):
| Producer | Opcodes | Globals |
|---|---|---|
| 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, STOP | none |
hashes.pkl | PROTO, EMPTY_DICT, MARK, BINUNICODE, NONE, NEWTRUE/NEWFALSE, BINFLOAT, BININT1/BININT, BINPUT, SETITEMS, STOP | none |
| REPLICATE body | idem hashes.pkl without valid/updated | none |
.pending records | PROTO, MARK, BINUNICODE, BININT1/BININT/LONG1, NONE, BINPUT, TUPLE, STOP | none |
| async pending | PROTO, EMPTY_DICT, MARK, BINUNICODE, BININT1, BINPUT, SETITEMS, STOP | none (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:
- Reject unknown opcodes and every
GLOBAL/STACK_GLOBALother than the allow-list; treat_codecs.encode(s, "latin1")+REDUCEas "bytes = latin-1 code points of s"; treatHeaderKeyDictreconstruction as a plain string map. - Keep a memo for
BINPUT/LONG_BINPUT/BINGET/LONG_BINGET; cap the input size (metadata is bounded byMAX_META_OVERALL_SIZEplus system keys; reject > 1 MiB) and the nesting depth. - For metadata: decode keys and values as UTF-8 with
surrogateescapesemantics (keep invalid bytes); if the pickle has no_codecs\nencodein its first 32 bytes it was written by Python 2: decode non-namevalues as latin-1 then UTF-8 (swift/obj/diskfile.py:171). - Verify
user.swift.metadata_checksumbefore decoding when present. - To write, emit exactly the Python 3 form (protocol 2,
_codecs.encodefor bytes) so that Python nodes,swift-object-infoand the auditor read it; the vectors give byte-exact expected outputs for given dicts. Python'spickledoes 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]. - The
.builderfiles are the exception: Swift itself loads them with an unrestricted unpickler. Keepswift-ring-builderin 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.
| DB | Tables | Notes |
|---|---|---|
| both | outgoing_sync, incoming_sync (remote_id UNIQUE, sync_point, updated_at) + triggers | replication high-water marks (swift/common/db.py:389) |
| account | container (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_stat | triggers keep account_stat counts and hash (swift/account/backend.py:73, swift/account/backend.py:125, swift/account/backend.py:158) |
| container | object (ROWID, name, created_at, size, content_type, etag, deleted, storage_policy_index), container_info + view container_stat (INSTEAD OF UPDATE trigger), policy_stat, shard_range | object 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. Thehashis compared across replicas by thesyncRPC. 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 sameALTER TABLEs. created_atof 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_typemay carry;swift_bytes=<n>(SLO/encryption real size), stripped from listings (swift/container/backend.py:229).metadatacolumn: 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>.dbduring sharding;get_db_statereturnsunsharded,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).
| DB | Tuple | Source |
|---|---|---|
| container | (name, created_at, size, content_type, etag, deleted, storage_policy_index, ctype_timestamp, meta_timestamp); older writers send 6 or 7 fields | swift/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_existingmerges 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_itemsapplies the same rule and updatesincoming_sync(swift/container/backend.py:1331).
6. Memcached
| Usage | Keys | Value | Source |
|---|---|---|---|
| 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 ranges | shard-listing-v2/<a>/<c>, shard-updating-v2/<a>/<c> | JSON list of namespaces | swift/proxy/controllers/base.py:641 |
| tempauth tokens | <prefix>/token/<token>, <prefix>/user/<account>:<user> | JSON | swift/common/middleware/tempauth.py:797 |
| ratelimit | ratelimit/<a>[/<c>] | counters (incr) | swift/common/middleware/ratelimit.py:160 |
| keystonemiddleware token cache | its own keys | keystonemiddleware |
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
| System | Protocol | Library | Direction | Usage |
|---|---|---|---|---|
| Keystone | REST (token validation, /v3/s3tokens) | keystonemiddleware authtoken in the proxy pipeline; s3token (swift/common/middleware/s3api/s3token.py:154) | proxy → Keystone | identity for keystoneauth, S3 signatures |
| Barbican / KMIP | REST / KMIP | castellan (kms_keymaster), PyKMIP (kmip_keymaster) | proxy → KMS at startup | root secret for encryption (swift/common/middleware/crypto/keymaster.py:227) |
| memcached | text protocol | swift/common/memcached.py | proxy → memcached | § 6 |
| statsd | UDP | swift/common/statsd_client.py | all → statsd | metrics (log_statsd_host) |
| syslog | UDP/unix socket | logging | all | logs |
| recon cache | JSON files /var/cache/swift/{object,container,account,drive,relinker}.recon | swift/common/recon.py:17 | daemons write, /recon/* of storage servers reads | replication/audit/update stats (swift/common/middleware/recon.py:424) |
rsync | process + rsync protocol | subprocess | replicators → peers | § 3 |
swift.conf | INI | — | all read | hash prefix/suffix, storage policies, constraints |
| other cluster (container sync) | HTTP to remote proxies | swift/container/sync.py:77 | container-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 pendingThe 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.