queueing: generalize DynamicClassifier for pull-based per-class structures - #1122
queueing: generalize DynamicClassifier for pull-based per-class structures#1122adamgeorge309 wants to merge 8 commits into
Conversation
| 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.
There was a problem hiding this comment.
Same as on the other thread: the history has been reorganized, so the commit named above is no longer part of the branch. The behavior is unchanged and now lives in createBranchModule(), which calls setSubmoduleVectorSize() with the maximum of the current size and the index it needs, so a vector declared larger in NED is never truncated.
The correction to the premise still stands: setSubmoduleVectorSize() refuses to shrink over a range that still holds submodules rather than deleting them, so the failure mode would have been an aborted run, not modules silently disappearing.
|
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? |
|
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 |
|
Pushed two commits.
One residue: a vector is declared in the result file when it is registered, so the discarded recorders leave an empty declaration behind under the temporary name.
Two related defects I did not touch here:
|
|
The inherited One detail worth flagging: the class index is now taken straight from the classifier function instead of going through The module test no longer needs the |
47ba427 to
ad7b15b
Compare
4a91aeb to
d86bc26
Compare
Invert the class lookup into an early return, so that the block creating the branch of a first-seen class sits at function level instead of inside the conditional. Whitespace-only except for the inverted condition and the hoisted return -- review with a whitespace-ignoring diff. No change in behavior. This puts the block in position for the next commit to move it out verbatim.
Extract-function move: the block that builds a branch -- grows the submodule vector, creates the module, wires it between the classifier and the multiplexer, and initializes it -- becomes createBranch(), the lines byte-identical (review with --color-moved). The class-to-branch map entry stays at the call site, fed by the return value: the map is classification bookkeeping, and createBranch() is topology only. No change in behavior. classifyPacket() reads as what it is: look the class up, create its branch on first sight.
~DynamicClassifier could only wire a branch into a submodule literally named "multiplexer". The downstream aggregator is now named by the aggregatorSubmoduleName parameter (still "multiplexer" by default), and it may be a pull scheduler instead of a push multiplexer: an aggregator that has to take notice of an input appearing at runtime learns about it from the POST_MODEL_CHANGE notification of the connection being made (cPostPathCreateNotification), so no contract is needed between the classifier and the aggregator beyond wiring the gate. For the pull side the classifier now also takes a collector reference per branch, the way it already took a consumer reference for the push side. The missing-submodule-vector and missing-aggregator cases fail with a clear error naming the module instead of a null dereference.
Branch modules were initialized right after being built, before the branch was connected to the aggregator, and the classifier took its sink references on its new out gate while the far end of the path was still incomplete. Both are traps for a compound branch: a module that resolves its downstream peer in initialize() would see a dangling gate, and ModuleRefByGate::reference() resolves the peer eagerly by walking the connection -- with mandatory=false it silently stores a nullptr that nothing ever re-resolves, leaving a permanently null consumer whose canPushPacket() throws and whose pushPacket() quietly degrades to send(), bypassing back-pressure. Wire first, resolve and initialize after: createBranchModule() builds the branch module (with its final name and index, so its parameters, display string and result recording are all resolved for the module path it keeps) and leaves it uninitialized; createBranch() connects the chain up to and including the aggregator, then takes the references and initializes the branch. The complete path is also the earliest point at which the packet operations of the branch can be checked, so the new gate now gets the checkPacketOperationSupport() that the base class gives every gate wired in NED. A branch type that does not support pushing is refused with the usual message instead of failing later on the first packet. No change in behavior for the existing simple-branch users, where the old order happened to be safe.
The class-to-branch map was keyed on the result of PacketClassifier::classifyPacket(), which maps the classifier function's index through getOutputGateIndex(). With reverseOrder that mapping is relative to the current number of output gates -- which grows with each branch created -- so the same class would be looked up under a different key later, miss, and get a second branch. Key the map on the classifier function's index directly, taken through the new getClassIndex(), which classifies without the branch-creating side effect of classifyPacket(). The map is renamed after what it now holds. Bypassing getOutputGateIndex() leaves reverseOrder with nothing to act on, so it is refused in initialize() instead of being silently ignored. Nothing is lost: the order of the output gates is the order in which the classes first appear, and no configuration that sets it works today -- that is the bug this commit fixes.
d86bc26 to
2f94d75
Compare
levy
left a comment
There was a problem hiding this comment.
I think dynamic packet classification doesn't belong to the PacketClassifierBase, it belongs to DynamicClassifier.
why do we add to PacketClassifierBase and change the meaning of classifyPacket?
virtual int createGateForPacket(Packet *packet);
virtual bool canCreateGateForPacket(Packet *packet) const;
why do we need to change? what do they have to do with dynamic packet classification?
BehaviorAggregateClassifier::canPushPacket
MultiFieldClassifier::canPushPacket
|
Why it went into the base The starting point was the earlier note that On changing the meaning of The -1 is not new. On master return index == -1 ? index : getOutputGateIndex(index);and four classifiers already produce it: You are right that it can stay in DynamicClassifier It comes to three overrides, because void DynamicClassifier::pushPacket(Packet *packet, const cGate *gate); // create the branch, then delegate
void DynamicClassifier::startPacketStreaming(Packet *packet); // the same, on the streaming path
bool DynamicClassifier::canPushPacket(Packet *packet, const cGate *gate) const; // look up only, never create
What it costs is that
Nothing — they have nothing to do with dynamic classification, and they are only there as collateral of the base change. Both return -1 for a packet that matches nothing and send it out of Proposal Move it into |
classifyPacket() built the branch of a class it had not seen before, so classifying a packet changed the model. Every path classifies: the capacity checks classify the packet they are asked about, the pull path classifies on every peek, and the delivery path classifies again -- so merely asking this classifier whether it could take a packet grew a gate vector, created a submodule, wired connections and initialized modules. classifyPacket() is a plain map lookup now, and the branch is created where the fate of a packet is actually decided: pushPacket() and startPacketStreaming() create it before delegating to the base class. They are the two doors of the delivery path -- the three streaming push operations all classify through startPacketStreaming(). canPushPacket() creates it and asks it. The answer has to hold for the very packet it is asked about, because a source that asks may push exactly that packet next, and a branch that does not exist yet cannot promise to take it: a branch whose first module is a closed gate refuses, and the packet is then pushed through a gate that is not open. This is the one query whose answer decides the fate of a packet, so it is the one query that may build what decides it. The pull side keeps classifying without creating, and a class that has no branch fails there with the base class's out-of-range error. It is not a supported configuration: a puller cannot ask an output gate that does not exist yet for a class that has never been seen.
canPushSomePacket() is inherited as "one of the existing branches can take a packet", which is false for a classifier that has not built any branch yet. An active source in front of such a classifier stops, waits for the notification that would tell it packets can be pushed again, and never gets it, because nothing else creates the first branch. Answer true while there is no branch: a packet of a class that has not been seen yet is taken by the branch created for it, and the range of the classifier function is not known here, so there may always be such a class. Only while there is no branch. This query has no packet, so it cannot create a branch and ask it the way canPushPacket() does, and an active source takes the answer as the licence to produce a packet and push it; answering true once the branches exist would push into a full branch, and a queue that has no packet dropper refuses that and fails the run. The inherited answer is the right one from the first branch on. It costs a full branch stopping a source that would have opened a new class, but stopping is the safe error, and it is what a statically wired classifier does in the same situation. Three queueing tests cover the module, which had none. The first builds two branches on demand -- its producer is connected to the classifier directly, so without this commit it never produces and no branch is built -- and covers the rest of the contract: an ini file assignment addressing a submodule of a branch takes effect, and the statistics of the branch submodules are recorded under the branch path. The second fills a branch and requires the producer to stop instead of overloading it. The third wires the branches into an aggregator that is not the one named by default.
2f94d75 to
55d4626
Compare
|
Done — the change is now confined to The branch is created in the three places that decide a packet's fate:
The cost is a
Verified: all eight commits build; |
DynamicClassifiercreates the branch of each traffic class the first time a packet of that class arrives. It could build only one shape — a submodule of the configured type in a preexisting submodule vector, wired to a submodule literally namedmultiplexer— and it built that branch as a side effect of classifying, which is supposed to be a query. This series generalizes it so that it can also build pull-based per-class structures, and fixes the defects that stood in the way.Everything is confined to
DynamicClassifier.PacketClassifierBaseand the meaning ofclassifyPacket()are untouched.This is the enabling change for the per-station airtime-fair IEEE 802.11 transmit queue, which is proposed separately on top of this branch.
What changes
classifyPacket()used to build the branch of a class it had not seen, so classifying changed the model — and every path classifies: the capacity checks classify the packet they are asked about, the pull path classifies on every peek, and delivery classifies again. It is a plain map lookup now. The branch is created where a packet's fate is decided: inpushPacket()andstartPacketStreaming(), the two doors of the delivery path, and incanPushPacket(), which creates the branch and asks it — the answer has to hold for the very packet it is asked about, because a source that asks may push exactly that packet next.aggregatorSubmoduleNameparameter, stillmultiplexerby default. It may be a pull scheduler instead of a push multiplexer: an aggregator that has to take notice of an input appearing at runtime learns about it from thePOST_MODEL_CHANGEnotification of the connection being made, so no contract is needed between the classifier and the aggregator beyond wiring the gate.getOutputGateIndex()maps relative to the current number of output gates, which grows with each branch — so withreverseOrderthe same class was looked up under a different key later and got a second branch. The map is keyed on the classifier function's index now, andreverseOrder, which this leaves with nothing to act on, is refused rather than silently ignored.canPushSomePacket()with "no", stopping an active source in front of it before the first branch was ever created, with no notification that could ever restart it. It answers "yes" while it has no branch, and falls back to the inherited answer from the first branch on — that query has no packet, so unlikecanPushPacket()it cannot create a branch and ask it, and an active source takes the answer as the licence to produce a packet and push it.Behavior of existing configurations is unchanged: the defaults reproduce the previous push-multiplexer shape. This is an argument from the defaults, not a measurement — neither in-tree user has a runnable configuration to measure.
MacServiceandPeerServiceare not instantiated anywhere underexamples/,showcases/,tests/ortutorials/, andtutorials/protocolNetwork90stops during network setup on unassigned parameters, on master as much as here. What the tests below do cover is both branch shapes those users have: a simple branch module and a compound one, each wired into a push multiplexer.Reading order
Eight commits. The first two are behavior-preserving preparation, and are meant to be read with a whitespace-ignoring diff (
git show -w) and with--color-movedrespectively. The three that follow fixDynamicClassifierin place. Then classification becomes side-effect free, then the back-pressure answer is repaired, and the WHATSNEW entry comes last.Architectural surface
PacketClassifierBase,classifyPacket()and every other classifier are untouched; the whole change isDynamicClassifieroverridingpushPacket(),startPacketStreaming(),canPushPacket()andcanPushSomePacket().canPushPacket()isconstand creates the branch it is asked about, through aconst_castmarked as a kludge, next to the onePacketClassifierBase::callClassifyPacket()already carries. It is the one query whose answer decides the fate of a specific packet, so it is the one query that may build what decides it.aggregatorSubmoduleNameparameter with a default;submoduleNameandmoduleTypeunchanged; the inheritedreverseOrderis refused byDynamicClassifier.AV-*orNV-*ledger rows, and no sealed path is touched.Tests
Three queueing tests are added. The module had none.
DynamicClassifier_1.test— two branches built on demand; an ini file assignment addressing a submodule of a branch takes effect; the statistics of the branch submodules are recorded under the branch path. Its producer is connected to the classifier directly, so it fails without the back-pressure commit.DynamicClassifier_2.test— an already created branch is filled up; the producer must stop rather than push into it. Verified to discriminate: reverting the commit makes it fail withQueue is overloaded without a packet dropper.DynamicClassifier_3.test— the branches are wired into an aggregator that is not the one named by default.Commands and results:
The twelve failures are the ones master fails as well, measured on
masterin the sameenvironment: the same twelve test names, in
Gate_*,PeriodicGate_1,RedDropper_1,Tagger_1,OrdinalBased*,TokenBucket*andMultiTokenBucket*. They expect EV log linesthat the current build no longer emits, in modules this branch does not touch.
doc/architecture/enforcement/check-architecture.sh src/inet/queueingreports the samepre-existing violations as master, and none in the changed files.
Checked separately, outside the test suite: a branch whose first module is a closed
PacketGate— a branch type that refuses its first packet. APacketServerupstream askscanPushPacket()and then pushes that exact packet; with the branch created and asked, theserver is told no and keeps the packet, and the run completes. Answering the query without
creating the branch pushes the packet through a gate that is not open.
No fingerprint or statistical baseline is affected: no fingerprint row covers
tutorials/protocolorsrc/inet/protocolelement, the only existingDynamicClassifierusers.