A hub collection that leads back to itself
A content collection with SourceType = "Hub" has no files of its own. HubStreamProviderFactory
asks the hub at its Address for the collection's config (GetDataRequest(ContentCollectionReference))
and builds the provider the answer names. The answer can be another Hub config, and the factory
resolves that one the same way.
🚨 The one sentence
If the chain of answers ever names a hop it has already asked, there is no answer, and the
resolution is refused with HubCollectionCycleException, which lists the hops.
How a cycle forms
The commonest shape needs no misconfiguration in any single place:
- A node has no collection called
exportsof its own. - Its content service falls back to its parent's (
ContentService.GetCollectionConfig). - The parent holds a
Hubconfig calledexportsnaming that node. A portal's/contentpage registers exactly that when someone opens the node'sexportscollection. - Asked for
exports, the node answers with the parent's config,Hubat itself.
Why it crashed a silo instead of failing a read
Each round is one more asynchronous request, so nothing fails while the loop runs and no stack
grows. Each round also nests one more SelectMany inner observer, because round n + 1 is the
inner observable of round n. When a terminal signal finally arrives, it travels through every
level on one stack.
On a memex portal pod on 2026-10-30, that signal was the ShuttingDown answer of an owner that
was quiescing (see Riding Out a Shutting-Down Address). It
travelled through 130,840 nested SelectMany+InnerObserver.OnError frames, and the process
died of a StackOverflowException. The crash dump then froze the process for about 42 s. Orleans'
LocalSiloHealthMonitor logged that freeze as .NET Thread Pool execution stalled for 42.2s
(MeshWeaver#5388).
Reading a stall that is a crash
The liveness heartbeat ([LIVENESS]) runs on its own thread. When it overran by about the stall's
length and almost none of that was GC pause, the whole process was suspended. Look for createdump
and Stack overflow. lines in the 30 s before the stall, and for the runtime's
Repeated N times: block, which names the recursing frame. Neither the thread pool nor the GC
caused it.
The fix and its proof
HubStreamProviderFactory carries the hops a resolution has asked, each as an (address,
collection) pair. A Hub answer is resolved on the same chain, and an answer that repeats a hop
errors on that hop. The two fields are compared exactly as the mesh compares them: Address
equality, and ordinal collection names, because the content service's lookup is case-sensitive.
A single {address}/{collection} string would refuse two legitimate chains. One is Exports →
exports on the same node. The other is a slash-qualified collection name, where host/1 +
folder/exports and host/1/folder + exports would print alike.
HubCollectionCycleTest pins the refusal and two controls, and each test guards the others:
- a self-referential inherited
Hubconfig errors with the chain[host/1 [inherited], host/1 [inherited]]; - a
Hubconfig the host serves from its own FileSystem collection still resolves; - a
Hubconfig namedServedthat redirects toservedon the same host still resolves.
What the dump proves
A governed AnalyzeDump of the 2026-10-30 dsfp6 dump ties the overflow to this factory.
dumpheap -stat held about 2.42 million instances of each of these types:
HubStreamProviderFactory+<>c__DisplayClass3_0, the closure of theSelectManyselector inCreate;SelectMany<IMessageDelivery<GetDataResponse>, IStreamProvider>+ObservableSelector+_and itsInnerObserver;- the matching
Func<…>,GetDataRequestandContentCollectionReference.
Those type arguments and that closure class belong only to HubStreamProviderFactory.Create. The
stack frames print generic arguments as __Canon, so they could not name the factory; the heap
statistics do.
Not established
- Which config made
exportson that node resolve back to itself. The node has since gone. The test reproduces one route, an inherited parentHubconfig like the one a/contentpage registers. - Whether any other reactive loop in the platform recurses through
SelectManyin the same way. A static rule for it does not exist yet.