Skip to content

Propose a new ADR for chunked router configuration - #2557

Closed
c-kruse wants to merge 1 commit into
skupperproject:mainfrom
c-kruse:adr-chunked-router-configuration
Closed

c-kruse wants to merge 1 commit into
skupperproject:mainfrom
c-kruse:adr-chunked-router-configuration

Conversation

@c-kruse

@c-kruse c-kruse commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Part of #2556

Summary by CodeRabbit

  • Documentation
    • Added an architecture decision record describing a scalable approach for Kubernetes router configuration.
    • Documents support for inline configurations and deterministic, content-addressed configuration chunks.
    • Covers publication ordering, retry behavior, cleanup of unused chunks, serialization consistency, and operational trade-offs.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds ADR 0011 for chunked Kubernetes router configuration. The proposal keeps small configurations inline and stores larger configurations as deterministic, content-addressed chunks with a digest-based head document.

Changes

Chunked router configuration

Layer / File(s) Summary
Transport proposal
doc/adr/0011-chunked-router-configuration.md
Documents inline and chunked representations, deterministic serialization, immutable chunk ConfigMaps, publication ordering, missing-chunk retries, garbage collection, and reconstruction.
Consequences and alternatives
doc/adr/0011-chunked-router-configuration.md
Records compatibility constraints, spill thresholds, inspection tooling needs, and alternatives such as compression, xDS, annotations, and persistent storage.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Mergeability Score: 🟡 Moderate · up to 691e2

The proposed configuration design still leaves unresolved risks around chunk deletion, size limits, lookup behavior, and deterministic output, which could cause configuration loss, publication failures, or inconsistent router configuration. The ADR is not ready to merge until these bounded correctness and implementation-contract issues are addressed.

Suggested reviewers: nluaces, fgiorgetti

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the new ADR for chunked router configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Signed-off-by: Christian Kruse <christian@c-kruse.com>
@c-kruse
c-kruse force-pushed the adr-chunked-router-configuration branch from fefddb9 to 691e22e Compare August 13, 2026 15:50
@c-kruse
c-kruse marked this pull request as ready for review August 13, 2026 16:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6000947a-156a-4c3b-82ca-69d4e1b32c75

📥 Commits

Reviewing files that changed from the base of the PR and between 8dbfca2 and 691e22e.

📒 Files selected for processing (1)
  • doc/adr/0011-chunked-router-configuration.md

Comment thread doc/adr/0011-chunked-router-configuration.md
Comment on lines +42 to +47
* Configuration is decomposed into keyed records (one per router entity).
* Records are partitioned deterministically into chunks by hashing their keys;
chunk count is derived from total size.
* Chunks are immutable (`ConfigMap.immutable: true`) and content-addressed: named and
referenced by the digest of their content.
* The head lists the digests of the complete chunk set.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- ADR outline ---'
ast-grep outline doc/adr/0011-chunked-router-configuration.md 2>/dev/null || true

printf '%s\n' '--- ADR lines 1-111 ---'
cat -n doc/adr/0011-chunked-router-configuration.md | sed -n '1,111p'

printf '%s\n' '--- ConfigMap-related implementation and tests ---'
rg -n -C 3 'ConfigMap|immutable|chunk|digest|1 ?MiB|1048576|config.*size|size.*config' internal doc test tests 2>/dev/null | sed -n '1,260p'

Repository: skupperproject/skupper

Length of output: 28693


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Chunking and router-configuration design references ---'
rg -n -C 4 'partitioned|hashing|chunk count|chunk size|head document|chunk ConfigMap|skrouterd.json|TransportConfigFile|1MiB|1 MiB|1048576' . \
  -g '!vendor/**' -g '!node_modules/**' -g '!dist/**' | sed -n '1,320p'

printf '%s\n' '--- Deterministic counterexample: count-based partitioning ---'
python3 - <<'PY'
import hashlib

limit = 1024 * 1024
records = {
    "small-a": b"x" * 100,
    "small-b": b"x" * 100,
    "large": b"x" * (limit + 1),
}
total = sum(len(v) for v in records.values())
chunk_count = max(1, (total + limit - 1) // limit)
buckets = [[] for _ in range(chunk_count)]

for key, value in records.items():
    bucket = int.from_bytes(hashlib.sha256(key.encode()).digest(), "big") % chunk_count
    buckets[bucket].append((key, len(value)))

print({"total_bytes": total, "chunk_count": chunk_count, "buckets": buckets})
print("oversized_bucket_possible:", any(sum(size for _, size in b) > limit for b in buckets))
PY

Repository: skupperproject/skupper

Length of output: 22620


🌐 Web query:

Kubernetes ConfigMap maximum size 1 MiB data field official documentation

💡 Result:

The official Kubernetes documentation explicitly states that a ConfigMap is not designed to hold large chunks of data and that the data stored in a ConfigMap cannot exceed 1 MiB [1][2]. This size limit applies to the entire object, including its metadata and all contained key-value pairs [3]. This constraint exists primarily because ConfigMaps are stored in etcd, the backing key-value store for Kubernetes, which has historically enforced size limits to ensure performance and stability [4][3]. If you need to store configuration data exceeding this 1 MiB limit, the official documentation recommends alternative approaches such as mounting a volume, using a separate database, or utilizing an external file service [1][2].

Citations:


Bound each chunk and the head by encoded size.

Hashing records into a count derived from total size does not bound any bucket. A large record can exceed Kubernetes’ 1 MiB ConfigMap limit. The head’s digest list can also exceed the limit as the chunk count grows. Size each serialized document with envelope overhead and margin, and define handling for records that cannot fit. Otherwise publication can fail, and the claim that transport no longer bounds configuration size is incorrect.

Source: MCP tools

Comment on lines +49 to +56
**Publishing is ordered for atomicity.** The controller creates any missing
chunks first, then updates the head in a single object write. The head update
is the atomic boundary: a reader that follows head → chunks always assembles
one consistent generation. Readers treat a missing chunk (deleted under them)
as a signal to re-read the head and retry.

**Unreferenced chunks are garbage-collected** after a grace period.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Protect pending and cross-head chunk references during garbage collection.

The publisher creates chunks before the head references them. A concurrent collector can classify a pending chunk as unreferenced and delete it before the head update. If digest-only names allow sharing, the collector must also scan every router-group head, including skupper-router and skupper-router-2. Add a pending-publication lease or marker and re-check all heads immediately before deletion.

Comment on lines +57 to +65
**Serialization becomes deterministic.** One logical configuration has exactly
one byte representation: Records are serialized as compact JSON in a fixed
order by key. Readers merge records from all chunks and apply this ordering, so
the inline and chunked representations produce identical skrouterd.json bytes.
Deterministic chunk serialization ensures logically identical chunk content has
the same digest.

Config-init and the kube-adaptor understand both representations; their
downstream behavior (complete config file, ordered AMQP reconciliation) is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- ADR ---'
cat -n doc/adr/0011-chunked-router-configuration.md | sed -n '1,115p'
printf '%s\n' '--- candidate files ---'
fd -t f . | rg '(^|/)(config_init|config_sync|.*router.*config|.*chunk.*|.*config.*test).*'
printf '%s\n' '--- symbols and references ---'
rg -n -C 4 'MarshalRouterConfig|skrouterd\.json|SslProfiles|ProxyProfiles|RouterConfig|chunk|Chunk' internal doc --glob '*.{go,md}' | sed -n '1,260p'
printf '%s\n' '--- outlines ---'
for f in $(fd -t f . internal | rg 'config_init\.go$|config_sync\.go$|.*config.*_test\.go$'); do
  echo "### $f"
  ast-grep outline "$f" 2>/dev/null || true
done

Repository: skupperproject/skupper

Length of output: 29973


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- config_init.go ---'
cat -n internal/kube/adaptor/config_init.go
printf '%s\n' '--- serializer definitions and calls ---'
rg -n -C 6 'func .*MarshalRouterConfig|MarshalRouterConfig|type RouterConfig struct|RouterConfig struct|json\.Marshal|Marshal.*Config' . --glob '!vendor/**' --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | sed -n '1,320p'
printf '%s\n' '--- RouterConfig files ---'
fd -t f . | rg '(^|/)(router|.*qdr.*|.*config.*)\.(go|json|yaml|yml)$' | sed -n '1,220p'

Repository: skupperproject/skupper

Length of output: 25694


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- RouterConfig definition ---'
cat -n internal/qdr/qdr.go | sed -n '1,180p'
printf '%s\n' '--- ConfigMap parsing and serialization ---'
cat -n internal/qdr/qdr.go | sed -n '780,1010p'
printf '%s\n' '--- serializer tests ---'
cat -n internal/qdr/qdr_test.go | sed -n '180,340p'
cat -n internal/qdr/qdr_test.go | sed -n '440,490p'
cat -n internal/qdr/qdr_test.go | sed -n '640,700p'
printf '%s\n' '--- relevant type definitions ---'
rg -n -C 3 'type (RouterMetadata|SslProfile|ProxyProfile|Listener|Connector|Address|RouterAccess|SiteConfig|RouterLogging|LogConfig) struct|json:".*omitempty|map\[string\]' internal/qdr/qdr.go | sed -n '1,360p'

Repository: skupperproject/skupper

Length of output: 35934


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

source = Path("internal/qdr/qdr.go").read_text()
marshal = source[source.index("func MarshalRouterConfig"):source.index("func AsConfigMapData")]
map_ranges = re.findall(r'for\s+_,\s+e\s*:=\s*range\s+([A-Za-z0-9_.]+)\s*\{', marshal)

records = {
    "SslProfiles": [["sslProfile", {"name": "a"}], ["sslProfile", {"name": "b"}]],
    "ProxyProfiles": [["proxyProfile", {"name": "a"}], ["proxyProfile", {"name": "b"}]],
}
def assembled(order):
    return '[\n' + ',\n'.join(f'  ["{kind}", {value}]' for kind, value in order) + '\n]'

forward = assembled(records["SslProfiles"])
reverse = assembled(list(reversed(records["SslProfiles"])))

print("MarshalRouterConfig map ranges:", ", ".join(map_ranges))
print("uses json.MarshalIndent:", "json.MarshalIndent(" in marshal)
print("sorts map keys:", bool(re.search(r'\b(sort|Sort|Sorted)\b', marshal)))
print("SslProfiles forward bytes:", repr(forward))
print("SslProfiles reverse bytes:", repr(reverse))
print("different record order produces different bytes:", forward != reverse)

tests = "\n".join(str(p) for p in Path(".").rglob("*_test.go") if p.is_file())
exact_byte_patterns = [
    r'bytes\.Equal',
    r'assert\.Equal\(.*\[\]byte',
    r'cmp\.Diff\(.*\[\]byte',
    r'expected.*skrouterd\.json',
]
print("exact-byte test indicators:", [
    pattern for pattern in exact_byte_patterns if re.search(pattern, tests, re.DOTALL)
])
print("adaptor test file exists:", Path("internal/kube/adaptor/config_init_test.go").exists())
PY

Repository: skupperproject/skupper

Length of output: 786


Define and test one canonical serializer for both representations.

qdr.MarshalRouterConfig iterates over configuration maps without sorting keys and uses json.MarshalIndent, so output bytes can vary. Specify the canonical formatting, record order, field omission, defaults, and escaping rules. Add a golden test that compares exact skrouterd.json bytes for inline and chunked inputs, including SslProfiles and ProxyProfiles.

Comment on lines +90 to +91
limits are also impractical: only 13 MultiKeyListeners, each using the maximum
256 routing keys of 64 bytes, exceed the 1 MiB limit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the MultiKeyListener capacity calculation.

13 × 256 × 64 equals 212,992 bytes, not more than 1 MiB. Raw key bytes reach 1 MiB at 64 such listeners, before serialization overhead. Correct the listener count or provide a calculation based on the actual serialized configuration size. (kubernetes.io)

Source: MCP tools

@nluaces

nluaces commented Aug 13, 2026

Copy link
Copy Markdown
Member

Minor thing to keep in mind: there is an ADR in #2541 with the same id: doc/adr/0011-multi-van-resources.md; whatever pr gets merged first, the other needs to update the name of the ADR file.

@c-kruse

c-kruse commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @nluaces, closing this issue. It looks like compressing the configuration alone is going to get us enough to fit any reasonable kubernetes Site. Following up with a replacement ADR.

@c-kruse c-kruse closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants