queueing: generalize DynamicClassifier for pull-based per-class structures - #1122
queueing: generalize DynamicClassifier for pull-based per-class structures#1122adamgeorge309 wants to merge 5 commits into
Conversation
Schedulers that get their inputs wired up at runtime -- rather than from the NED topology -- need a way to be told about a newly connected input gate, so that they start considering it. ~DynamicClassifier creates and connects the gate; addInput() is how the scheduler learns about it. This makes it possible for a dynamic classifier to build pull-based per-class structures (queue + scheduler), not just push-based demux/remux chains terminating in a ~PacketMultiplexer.
~DynamicClassifier could only build one shape: create a submodule of the configured type in a preexisting submodule vector, and wire it to a submodule literally named "multiplexer". Three generalizations: - The downstream aggregator is now named by the aggregatorSubmoduleName parameter (still "multiplexer" by default). If it implements ~IDynamicInputScheduler, the newly created input gate is registered with it, so the aggregator can be a pull scheduler instead of a push multiplexer. - With spliceBranchSubmodules, a compound moduleType forming a linear in -> a -> b -> ... -> out chain is flattened: its inner submodules are reparented into the classifier's parent as vector elements named after them (a[k], b[k]), rather than staying behind the compound's boundary. A downstream matched-pair scheduler can then address them directly, which a compound boundary would prevent. - forwardMatchingParams() copies same-named parameters from the enclosing module into the created branch before it is finalized, so a per-class compound picks up the enclosing queue's configuration. Branch module initialization is deferred until the whole chain, including the aggregator connection, is wired -- a module that resolves its downstream peer in initialize() would otherwise see a dangling gate. submoduleName is now optional (it is unused when splicing), and the missing-submodule-vector and missing-aggregator cases fail with a clear error instead of a null dereference.
| outputGates.push_back(classifierOutputGate); | ||
| PassivePacketSinkRef consumer; | ||
| consumer.reference(classifierOutputGate, false); | ||
| consumers.push_back(consumer); | ||
| ActivePacketSinkRef collector; | ||
| collector.reference(classifierOutputGate, false); | ||
| collectors.push_back(collector); |
There was a problem hiding this comment.
🔴 Newly created traffic-class branches are never linked to the classifier, so packets of a new class break delivery
The downstream target of a newly added output is looked up (consumer.reference(...) at src/inet/queueing/classifier/DynamicClassifier.cc:56) before the branch is actually attached to that output, so the classifier ends up with no known destination for that class.
Impact: The first packet of every new traffic class either aborts the simulation with a null-reference error or silently falls back to a different delivery path, breaking normal flow control.
Reference resolution happens on a still-unconnected gate
ModuleRefByGate::reference(gate, false) (src/inet/common/ModuleRefByGate.h:80-89) resolves the peer immediately by walking gate->getNextGate() (src/inet/common/ModuleAccess.h:126-134). At DynamicClassifier.cc:55-60 the new out[index] gate has just been created by setGateSize() and is not connected yet -- the connection is only made later inside createModuleBranch() (src/inet/queueing/classifier/DynamicClassifier.cc:98) or spliceBranch() (src/inet/queueing/classifier/DynamicClassifier.cc:140). With mandatory == false the lookup silently yields nullptr, and the references are never re-resolved.
Consequences in PacketClassifierBase:
canPushSomePacket()/canPushPacket()(src/inet/queueing/base/PacketClassifierBase.cc:86-98) call intoconsumers[i], whosecheckReference()throws"Dereferencing nullptr...".pushPacket()usespushOrSendPacket()(src/inet/queueing/base/PacketProcessorBase.cc:118-126), which falls back tosend()when the consumer is null, bypassing the synchronous push API and back-pressure.
The previous implementation connected the gate first and only then created the reference, so it resolved correctly. The fix is to move the outputGates/consumers/collectors bookkeeping after the branch (and aggregator) wiring is done.
Prompt for agents
In DynamicClassifier::createBranch() (src/inet/queueing/classifier/DynamicClassifier.cc), the PassivePacketSinkRef and ActivePacketSinkRef for the new out[index] gate are resolved via reference(gate, false) immediately after setGateSize(), while the gate is still unconnected. ModuleRefByGate::reference() resolves the peer eagerly by following the connection, so with mandatory==false both refs become nullptr permanently. The gate is only connected later, in createModuleBranch()/spliceBranch(). This makes consumers[index] null, which makes PacketClassifierBase::canPushPacket()/canPushSomePacket() throw and makes pushPacket() fall back to send() instead of the push API. Restructure createBranch() so that outputGates/consumers/collectors are populated only after the branch has been created and classifierOutputGate has been connected to the branch input (the previous implementation connected first, then referenced). Note outputGates must stay index-aligned with consumers/collectors.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in f03585e — the references are now taken after the branch and the aggregator connection are wired, so reference() resolves against a connected path instead of storing a silent nullptr.
There was a problem hiding this comment.
Fixed in f03585e590 — the sink references are now taken after the branch is wired through to the aggregator. They resolve the far end of the path eagerly, so taking them earlier resolved them against a gate that was not connected yet.
| cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector<cModule *>& modulesToInitialize) | ||
| { | ||
| cModule *parent = getParentModule(); | ||
| parent->setSubmoduleVectorSize(submoduleName, index + 1); |
There was a problem hiding this comment.
🟡 Creating the first per-class branch can delete pre-existing branch modules declared in the network description
The branch container is resized to exactly the new branch position (setSubmoduleVectorSize(submoduleName, index + 1) at src/inet/queueing/classifier/DynamicClassifier.cc:96) instead of only ever growing it, so any pre-existing branches beyond that position are destroyed.
Impact: Statically configured per-class branches can silently disappear at runtime, so traffic that should flow through them is lost or the run aborts.
Removal of the std::max() guard
The previous code deliberately used parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)) so the vector was never shrunk. The new code passes index + 1 unconditionally. index is the classifier's current out gate count, which is not necessarily >= the NED-declared vector size (e.g. a parent declaring defragmenter[numDefragmenter] whose classifier out gate vector was sized independently). Shrinking an existing submodule vector deletes the elements above the new size. The same unguarded resize is repeated in the splice path at src/inet/queueing/classifier/DynamicClassifier.cc:133.
| parent->setSubmoduleVectorSize(submoduleName, index + 1); | |
| parent->setSubmoduleVectorSize(submoduleName, std::max(parent->getSubmoduleVectorSize(submoduleName), index + 1)); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 680d9c8, via a grow-only helper at both resize sites. One correction to the premise: setSubmoduleVectorSize() refuses to shrink over a range that still holds submodules rather than deleting them, so the failure mode was an aborted run, not modules silently disappearing.
There was a problem hiding this comment.
Fixed in 680d9c8a13 — growSubmoduleVector() takes the max of the current and required size, so a vector declared larger in NED is never truncated.
|
The IDynamicInputScheduler interface has no implementors, how does this work? What's the point of having this interface? Why doesn't the module use the signals emitted when a gate gets connected? |
…ange notification An aggregator that has to take notice of an input appearing at runtime can learn about it from the POST_MODEL_CHANGE notification of the connection being made (cPostPathCreateNotification), so ~DynamicClassifier needs no contract with it beyond wiring the gate. Drop the interface and the dynamic_cast that went with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ted gate The PassivePacketSinkRef/ActivePacketSinkRef of a newly created branch were resolved right after setGateSize(), while the new out[index] gate was still unconnected -- the branch and the aggregator are only wired afterwards. ModuleRefByGate::reference() resolves the peer eagerly by walking the connection, and with mandatory=false it stores a nullptr without complaint, which nothing ever re-resolves. The result was a permanently null consumers[index]: canPushPacket() and canPushSomePacket() threw on the null dereference, and pushPacket() quietly degraded to send(), bypassing the push API and its back-pressure. Take the references only after the whole path, up to and including the aggregator connection, has been wired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vector was resized to exactly index + 1, where index is the classifier's current out gate count -- not necessarily at least the size the vector was declared with in NED. setSubmoduleVectorSize() refuses to remove a range that still holds submodules, so a parent declaring a larger vector than the classifier has output gates would abort the run. Route both resize sites through growSubmoduleVector(), which only ever extends, and which reports a missing vector by name (the splice path resizes vectors named after the spliced submodules, which initialize() cannot check up front). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both fair points — fixed. The interface is gone. Its only implementor lived in the follow-up airtime-fairness branch, so within this PR it was an orphan contract. And the notification is the better mechanism: an aggregator that needs to notice a runtime-added input now picks it up from the Two more, from the bot review:
Pushed as three commits on top. |
|
A separate defect in the splice path, not covered by any thread above. Spliced branches record all their vectors under the temporary compound's path, so per-station vectors are indistinguishable. In a 4-station run, all four sub-queues emit the same vector name — four ids, one name:
Deferring A fix needs each branch module to have its final parent and name before |
DynamicClassifiercould only build one shape: create a submodule of the configured type in a preexisting submodule vector, and wire it to a submodule literally namedmultiplexer. This generalizes it along three axes so it can also build pull-based per-class structures.aggregatorSubmoduleName(stillmultiplexerby default). If it implements the newIDynamicInputSchedulercontract, the runtime-created input gate is registered with it — so the aggregator can be a pull scheduler instead of a push multiplexer.spliceBranchSubmodules, a compoundmoduleTypeforming a linearin -> a -> b -> ... -> outchain is flattened: its inner submodules are reparented into the classifier's parent as vector elements named after them (a[k],b[k]). A downstream matched-pair scheduler can then address them directly, which a compound boundary would prevent.forwardMatchingParams()copies same-named parameters from the enclosing module into the created branch before it is finalized.Branch module initialization is deferred until the whole chain — including the aggregator connection — is wired, since a module that resolves its downstream peer in
initialize()would otherwise see a dangling gate.submoduleNamebecomes optional (unused when splicing), and the missing-vector / missing-aggregator cases now fail with a clear error instead of a null dereference.Behaviour of existing configurations is unchanged: the defaults reproduce the previous push-multiplexer shape.
This is the enabling change for the per-station airtime-fair 802.11 transmit queue, which is proposed separately on top of this branch.
Test
Builds at every commit. Existing
DynamicClassifierusers are unaffected by construction (defaults unchanged).🤖 Generated with Claude Code