Skip to content

Per-table msgpackr structure dictionaries grow unbounded and unobservably, with a wire-format cliff at id 63 #2220

Description

@Devin-Holland

Summary

A table's msgpackr record-structure dictionary appears to grow monotonically for the life of the table, with no cap, no pruning, no renumbering, and no way for an application or operator to observe its size. On one long-lived table in an internal service it has reached 1,297 typed structures, and there is a known wire-format boundary at id 63 that it has long since crossed.

Filing this as a design question rather than a defect report: is bounding this feasible, and if it is intentionally unbounded, what should applications avoid?

How the growth happens

A structure is a physical record layout — field set, field order, and a per-field enumerationOffset key-order fixup (msgpackr/struct.js: let [ type, size, key, enumerationOffset ] = definition). Two consequences:

  • Distinct field subsets each mint a structure. An application that patches partial field sets (a supported API) mints one per realized combination.
  • The same field set can mint several structures, because the enumeration offset depends on the target record's existing field population. Observed in one table's dictionary: the same field, same type, at offsets -9, -14 and -10 — three structures for one field.

So the dictionary grows with (realized field subsets) × (offset arrangements encountered), not with column count, and it only ever grows.

Why it matters

  1. There is a cliff at id 63. Structures 0–63 are referenced by msgpackr's one-byte inline token (0x40–0x7f, token & 0x3f); beyond that references use createSecondByteReader. In a mixed-version replication pair those two paths do not behave identically — the inline path guards a missing structure (currentUnpackr.getStructures && loadStructures()), the two-byte path calls loadStructures() unguarded — so a peer that was not sent the structure gets a TypeError and that table stops replicating (Mixed-version replication: 4.7 receiver throws TypeError on structure ids >= 64 sent by 5.2 harper-pro#729). A bounded dictionary would keep tables on the safe side of a boundary they currently cross silently.
  2. It is unobservable. Nothing reports the count. We discovered it by counting entries in a decoder dump inside an error message. There is no metric or admin surface that would let anyone notice a hot write path minting shapes, or verify that a mitigation worked.
  3. Every application-side mitigation is a workaround. Nesting volatile fields into one object, always writing a fixed complete field set, or periodically rewriting rows in a canonical shape all mean shaping writes around an encoder implementation detail — and none of them help the tables that already grew, nor the next table that repeats the pattern.

What I could not determine from the source

I looked at resources/RecordEncoder.ts (saveStructures, the durable Symbol.for('structures') key), RocksTransactionLogStore.ts and replayLogs.ts, and found no path that prunes or renumbers structures; I did not find compaction code that references them. That is a read of the code I looked at, not an exhaustive search — if such a path exists, this issue is really just the observability half.

I also recognise there may be a deliberate reason the dictionary is append-only: historical audit-log entries and replication backlogs reference structure ids, so naive pruning would make them undecodable. That argues for renumber-or-rebuild rather than delete, and it is exactly the trade-off I would rather have judged by someone who owns this design.

Questions

  1. Is unbounded growth intended, or is a bound/compaction feasible (e.g. renumbering live shapes during a rebuild, so ids track shapes still in use)?
  2. Could the per-table structure count be exposed as a metric or admin field, so applications can see this before it bites?
  3. If it is intended to be unbounded, what write patterns should applications avoid, and is the id-63 boundary something they should be expected to stay under?

Context

Found while debugging a rolling 4.7 → 5.2 upgrade of a two-node replicating pair: the structure count is what put the affected table past the boundary that triggers harper-pro#729. Application-side tracking issue: HarperFast/central-manager#695.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

Priority

P1

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions