From b2277151439d815ddca5812b0608db92afde66e0 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 6 Aug 2026 14:55:31 +0200 Subject: [PATCH 1/8] queueing: add the IDynamicInputScheduler contract 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. --- .../contract/IDynamicInputScheduler.h | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/inet/queueing/contract/IDynamicInputScheduler.h diff --git a/src/inet/queueing/contract/IDynamicInputScheduler.h b/src/inet/queueing/contract/IDynamicInputScheduler.h new file mode 100644 index 00000000000..eea21bb3ba1 --- /dev/null +++ b/src/inet/queueing/contract/IDynamicInputScheduler.h @@ -0,0 +1,38 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_IDYNAMICINPUTSCHEDULER_H +#define __INET_IDYNAMICINPUTSCHEDULER_H + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace queueing { + +/** + * Interface for packet schedulers whose set of inputs grows at runtime, so that a dynamic + * classifier (see ~DynamicClassifier) can add an input branch on demand. The classifier + * creates and connects the new input gate; the scheduler is then told to start considering + * it via addInput(). + */ +class INET_API IDynamicInputScheduler +{ + public: + virtual ~IDynamicInputScheduler() {} + + /** + * Registers an input gate that was created and connected at runtime: the scheduler sets + * up the corresponding provider reference and notifies its downstream collector that a + * new packet may become available. + */ + virtual void addInput(cGate *inputGate) = 0; +}; + +} // namespace queueing +} // namespace inet + +#endif From eea77e76bcb766f15d12cbdda8e11cc8a6b01b61 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 6 Aug 2026 14:55:46 +0200 Subject: [PATCH 2/8] queueing: generalize DynamicClassifier ~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. --- .../queueing/classifier/DynamicClassifier.cc | 141 ++++++++++++++---- .../queueing/classifier/DynamicClassifier.h | 31 +++- .../queueing/classifier/DynamicClassifier.ned | 15 +- 3 files changed, 153 insertions(+), 34 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 43d6fe71c02..a207fe7932e 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -9,6 +9,7 @@ #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" +#include "inet/queueing/contract/IDynamicInputScheduler.h" namespace inet { namespace queueing { @@ -21,44 +22,126 @@ void DynamicClassifier::initialize(int stage) if (stage == INITSTAGE_LOCAL) { submoduleName = par("submoduleName"); moduleType = cModuleType::get(par("moduleType")); - if (!getParentModule()->hasSubmoduleVector(submoduleName)) - throw cRuntimeError("The submodule vector '%s' missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); - } + aggregatorSubmoduleName = par("aggregatorSubmoduleName"); + spliceBranchSubmodules = par("spliceBranchSubmodules"); + if (!spliceBranchSubmodules && (submoduleName == nullptr || submoduleName[0] == '\0')) + throw cRuntimeError("The submoduleName parameter must be set when spliceBranchSubmodules is false (it names the submodule vector that holds each branch)"); + if (!spliceBranchSubmodules && !getParentModule()->hasSubmoduleVector(submoduleName)) + throw cRuntimeError("The submodule vector '%s' is missing from %s", submoduleName, getParentModule()->getFullPath().c_str()); + if (getParentModule()->getSubmodule(aggregatorSubmoduleName) == nullptr) + throw cRuntimeError("The aggregator submodule '%s' is missing from %s", aggregatorSubmoduleName, getParentModule()->getFullPath().c_str()); + } } int DynamicClassifier::classifyPacket(Packet *packet) { int index = PacketClassifier::classifyPacket(packet); auto it = classIndexToGateItMap.find(index); - if (it == classIndexToGateItMap.end()) { - auto parentModule = getParentModule(); - int submoduleIndex = gateSize("out"); - int origVectorSize = parentModule->getSubmoduleVectorSize(submoduleName); - parentModule->setSubmoduleVectorSize(submoduleName, std::max(origVectorSize, submoduleIndex + 1)); - auto module = moduleType->create(submoduleName, parentModule, submoduleIndex); - auto moduleInputGate = module->gate("in"); - auto moduleOutputGate = module->gate("out"); - auto multiplexer = parentModule->getSubmodule("multiplexer"); - multiplexer->setGateSize("in", multiplexer->gateSize("in") + 1); - auto multiplexerInputGate = multiplexer->gate("in", multiplexer->gateSize("in") - 1); - setGateSize("out", submoduleIndex + 1); - auto classifierOutputGate = gate("out", gateSize("out") - 1); - classifierOutputGate->connectTo(moduleInputGate); - outputGates.push_back(classifierOutputGate); - PassivePacketSinkRef consumer; - consumer.reference(classifierOutputGate, false); - consumers.push_back(consumer); - moduleOutputGate->connectTo(multiplexerInputGate); - module->finalizeParameters(); - module->buildInside(); + if (it != classIndexToGateItMap.end()) + return it->second; + int branchIndex = createBranch(); + classIndexToGateItMap[index] = branchIndex; + return branchIndex; +} + +int DynamicClassifier::createBranch() +{ + cModule *parent = getParentModule(); + int index = gateSize("out"); + // grow this classifier's output gate vector + setGateSize("out", index + 1); + cGate *classifierOutputGate = gate("out", index); + outputGates.push_back(classifierOutputGate); + PassivePacketSinkRef consumer; + consumer.reference(classifierOutputGate, false); + consumers.push_back(consumer); + ActivePacketSinkRef collector; + collector.reference(classifierOutputGate, false); + collectors.push_back(collector); + // build the branch and collect the modules whose initialization is deferred until the + // whole chain (including the aggregator connection) is wired + std::vector modulesToInitialize; + cGate *branchOutputGate = spliceBranchSubmodules ? + spliceBranch(index, classifierOutputGate, modulesToInitialize) : + createModuleBranch(index, classifierOutputGate, modulesToInitialize); + // wire the branch output into the aggregator's next input gate + cModule *aggregator = parent->getSubmodule(aggregatorSubmoduleName); + aggregator->setGateSize("in", aggregator->gateSize("in") + 1); + cGate *aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); + branchOutputGate->connectTo(aggregatorInputGate); + for (auto module : modulesToInitialize) module->callInitialize(); - classIndexToGateItMap[index] = submoduleIndex; - return submoduleIndex; + // if the aggregator is a pull scheduler with runtime-added inputs, register the new one + if (auto scheduler = dynamic_cast(aggregator)) + scheduler->addInput(aggregatorInputGate); + return index; +} + +void DynamicClassifier::forwardMatchingParams(cModule *module) +{ + // copy the value of every parameter the created module shares (by name) with this + // classifier's parent -- so e.g. a per-class compound picks up the enclosing queue's + // configuration before it (and its own submodules) are finalized + cModule *parent = getParentModule(); + for (int i = 0; i < module->getNumParams(); i++) { + cPar& param = module->par(i); + if (parent->hasPar(param.getName())) + param = parent->par(param.getName()); } - else - return it->second; +} + +cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize) +{ + cModule *parent = getParentModule(); + parent->setSubmoduleVectorSize(submoduleName, index + 1); + cModule *module = moduleType->create(submoduleName, parent, index); + classifierOutputGate->connectTo(module->gate("in")); + module->finalizeParameters(); + module->buildInside(); + modulesToInitialize.push_back(module); + return module->gate("out"); +} + +cGate *DynamicClassifier::spliceBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize) +{ + cModule *parent = getParentModule(); + // build the branch template compound + cModule *compound = moduleType->create("splicetmp", parent); + forwardMatchingParams(compound); + compound->finalizeParameters(); + compound->buildInside(); + // walk the linear inner chain: compound.in -> chain[0].in, chain[i].out -> chain[i+1].in, + // chain.back().out -> compound.out. Each chain module is expected to have a single in/out. + std::vector chain; + std::vector names; + for (cGate *g = compound->gate("in")->getNextGate(); g != nullptr && g->getOwnerModule() != compound; g = g->getOwnerModule()->gate("out")->getNextGate()) { + chain.push_back(g->getOwnerModule()); + names.push_back(g->getOwnerModule()->getName()); + } + if (chain.empty()) + throw cRuntimeError("spliceBranchSubmodules: compound '%s' has no linear in->out submodule chain", moduleType->getFullName()); + // disconnect all internal connections so the submodules can be reparented + compound->gate("in")->disconnect(); + for (cModule *module : chain) + if (module->gate("out")->isConnectedOutside()) + module->gate("out")->disconnect(); + // reparent each chain module into parent.[index]: move the scalar under a temporary + // unique name (so it does not clash with the target vector), then rename it into the slot + for (size_t i = 0; i < chain.size(); i++) { + cModule *module = chain[i]; + module->setName(("splicetmp_" + names[i]).c_str()); + parent->setSubmoduleVectorSize(names[i].c_str(), index + 1); + module->changeParentTo(parent); + module->setNameAndIndex(names[i].c_str(), index); + modulesToInitialize.push_back(module); + } + compound->deleteModule(); + // rewire the chain at the branch level + classifierOutputGate->connectTo(chain.front()->gate("in")); + for (size_t i = 0; i + 1 < chain.size(); i++) + chain[i]->gate("out")->connectTo(chain[i + 1]->gate("in")); + return chain.back()->gate("out"); } } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index e1907fe1e55..2af47330c5d 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -8,6 +8,8 @@ #ifndef __INET_DYNAMICCLASSIFIER_H #define __INET_DYNAMICCLASSIFIER_H +#include + #include "inet/queueing/classifier/PacketClassifier.h" namespace inet { @@ -15,20 +17,43 @@ namespace queueing { using namespace inet::queueing; +/** + * A packet classifier that creates the branch for each traffic class on demand, the first + * time a packet of that class is seen. Each branch is one submodule of a configurable type + * (moduleType), wired between this classifier's output and a downstream aggregator submodule + * (aggregatorSubmoduleName). + * + * The aggregator may be either a push ~PacketMultiplexer (the traditional use) or a pull + * scheduler; if it implements ~IDynamicInputScheduler the newly created input is registered + * with it via addInput(). This lets the same classifier build both push demux/remux chains + * and pull per-class queue/scheduler structures. + * + * If spliceBranchSubmodules is set and moduleType is a compound module forming a linear + * chain (in -> a -> b -> ... -> out), its inner submodules are *spliced* directly into this + * classifier's parent (as vector elements named after the inner submodules) instead of being + * kept behind the compound's boundary -- so a downstream matched-pair scheduler can address + * them directly. See the corresponding NED file for more details. + */ class INET_API DynamicClassifier : public PacketClassifier { protected: - const char *submoduleName = nullptr; - cModuleType *moduleType = nullptr; + const char *submoduleName = nullptr; // submodule vector for the branch module (non-splice); unused when splicing + cModuleType *moduleType = nullptr; // type of the per-class branch module (may be a compound) + const char *aggregatorSubmoduleName = nullptr; // downstream aggregator submodule (multiplexer or scheduler) + bool spliceBranchSubmodules = false; // flatten the (compound) branch module's submodules into the branch std::map classIndexToGateItMap; protected: virtual void initialize(int stage) override; virtual int classifyPacket(Packet *packet) override; + + virtual int createBranch(); + virtual void forwardMatchingParams(cModule *module); + virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); + virtual cGate *spliceBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/classifier/DynamicClassifier.ned b/src/inet/queueing/classifier/DynamicClassifier.ned index 9a679e097ce..efdb8a26bb5 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -7,10 +7,21 @@ package inet.queueing.classifier; +// +// A packet classifier that creates the branch for each traffic class on demand. Each branch +// is a submodule of type `moduleType`, wired between this classifier's output and a downstream +// aggregator submodule (`aggregatorSubmoduleName`, a push multiplexer by default). When the +// aggregator is a pull scheduler implementing ~IDynamicInputScheduler, the new input is +// registered with it. With `spliceBranchSubmodules`, a compound `moduleType` forming a linear +// in->...->out chain is flattened: its inner submodules are instantiated directly in the +// parent (as vector elements named after them) so a matched-pair scheduler can address them. +// simple DynamicClassifier extends PacketClassifier { parameters: - string submoduleName; - string moduleType; + string moduleType; // NED type of the per-class branch module (may be a compound) + string submoduleName = default(""); // submodule vector for the branch module (non-splice); unused when splicing + string aggregatorSubmoduleName = default("multiplexer"); // downstream aggregator submodule to wire branches into + bool spliceBranchSubmodules = default(false); // if true, flatten the compound moduleType's submodules into the branch @class(DynamicClassifier); } From 1f2dd8ba5afadbbfe2279f190ed228a95a083319 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 10 Aug 2026 15:49:36 +0200 Subject: [PATCH 3/8] queueing: replace the IDynamicInputScheduler contract with a model change 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) --- .../queueing/classifier/DynamicClassifier.cc | 9 ++--- .../queueing/classifier/DynamicClassifier.h | 8 ++-- .../queueing/classifier/DynamicClassifier.ned | 8 ++-- .../contract/IDynamicInputScheduler.h | 38 ------------------- 4 files changed, 14 insertions(+), 49 deletions(-) delete mode 100644 src/inet/queueing/contract/IDynamicInputScheduler.h diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index a207fe7932e..b1ece2a0838 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -9,7 +9,6 @@ #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" -#include "inet/queueing/contract/IDynamicInputScheduler.h" namespace inet { namespace queueing { @@ -64,16 +63,16 @@ int DynamicClassifier::createBranch() cGate *branchOutputGate = spliceBranchSubmodules ? spliceBranch(index, classifierOutputGate, modulesToInitialize) : createModuleBranch(index, classifierOutputGate, modulesToInitialize); - // wire the branch output into the aggregator's next input gate + // Wire the branch output into the aggregator's next input gate. An aggregator that has + // to take notice of a runtime-added input (a pull scheduler, for example) learns about + // it from the model change notification of this very connection, so nothing here needs + // to know what kind of aggregator it is. cModule *aggregator = parent->getSubmodule(aggregatorSubmoduleName); aggregator->setGateSize("in", aggregator->gateSize("in") + 1); cGate *aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); branchOutputGate->connectTo(aggregatorInputGate); for (auto module : modulesToInitialize) module->callInitialize(); - // if the aggregator is a pull scheduler with runtime-added inputs, register the new one - if (auto scheduler = dynamic_cast(aggregator)) - scheduler->addInput(aggregatorInputGate); return index; } diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index 2af47330c5d..f8ec854dc6b 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -24,9 +24,11 @@ using namespace inet::queueing; * (aggregatorSubmoduleName). * * The aggregator may be either a push ~PacketMultiplexer (the traditional use) or a pull - * scheduler; if it implements ~IDynamicInputScheduler the newly created input is registered - * with it via addInput(). This lets the same classifier build both push demux/remux chains - * and pull per-class queue/scheduler structures. + * scheduler. An aggregator that needs to take notice of an input appearing at runtime picks + * it up from the POST_MODEL_CHANGE notification of the connection itself (see + * cPostPathCreateNotification), so no extra contract is needed between the two. This lets the + * same classifier build both push demux/remux chains and pull per-class queue/scheduler + * structures. * * If spliceBranchSubmodules is set and moduleType is a compound module forming a linear * chain (in -> a -> b -> ... -> out), its inner submodules are *spliced* directly into this diff --git a/src/inet/queueing/classifier/DynamicClassifier.ned b/src/inet/queueing/classifier/DynamicClassifier.ned index efdb8a26bb5..615acdad024 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.ned +++ b/src/inet/queueing/classifier/DynamicClassifier.ned @@ -10,9 +10,11 @@ package inet.queueing.classifier; // // A packet classifier that creates the branch for each traffic class on demand. Each branch // is a submodule of type `moduleType`, wired between this classifier's output and a downstream -// aggregator submodule (`aggregatorSubmoduleName`, a push multiplexer by default). When the -// aggregator is a pull scheduler implementing ~IDynamicInputScheduler, the new input is -// registered with it. With `spliceBranchSubmodules`, a compound `moduleType` forming a linear +// aggregator submodule (`aggregatorSubmoduleName`, a push multiplexer by default). The +// aggregator may also be a pull scheduler; one that has to take notice of an input appearing +// at runtime learns about it from the model change notification of the connection being made, +// so no extra contract is needed between the two. +// With `spliceBranchSubmodules`, a compound `moduleType` forming a linear // in->...->out chain is flattened: its inner submodules are instantiated directly in the // parent (as vector elements named after them) so a matched-pair scheduler can address them. // diff --git a/src/inet/queueing/contract/IDynamicInputScheduler.h b/src/inet/queueing/contract/IDynamicInputScheduler.h deleted file mode 100644 index eea21bb3ba1..00000000000 --- a/src/inet/queueing/contract/IDynamicInputScheduler.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (C) 2026 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#ifndef __INET_IDYNAMICINPUTSCHEDULER_H -#define __INET_IDYNAMICINPUTSCHEDULER_H - -#include "inet/common/INETDefs.h" - -namespace inet { -namespace queueing { - -/** - * Interface for packet schedulers whose set of inputs grows at runtime, so that a dynamic - * classifier (see ~DynamicClassifier) can add an input branch on demand. The classifier - * creates and connects the new input gate; the scheduler is then told to start considering - * it via addInput(). - */ -class INET_API IDynamicInputScheduler -{ - public: - virtual ~IDynamicInputScheduler() {} - - /** - * Registers an input gate that was created and connected at runtime: the scheduler sets - * up the corresponding provider reference and notifies its downstream collector that a - * new packet may become available. - */ - virtual void addInput(cGate *inputGate) = 0; -}; - -} // namespace queueing -} // namespace inet - -#endif From f03585e59082512e0d424bf0f6eb7b84b9e25732 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 10 Aug 2026 15:50:11 +0200 Subject: [PATCH 4/8] queueing: fix DynamicClassifier taking sink references on an unconnected 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) --- .../queueing/classifier/DynamicClassifier.cc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index b1ece2a0838..4dd8b54bb78 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -50,13 +50,6 @@ int DynamicClassifier::createBranch() // grow this classifier's output gate vector setGateSize("out", index + 1); cGate *classifierOutputGate = gate("out", index); - outputGates.push_back(classifierOutputGate); - PassivePacketSinkRef consumer; - consumer.reference(classifierOutputGate, false); - consumers.push_back(consumer); - ActivePacketSinkRef collector; - collector.reference(classifierOutputGate, false); - collectors.push_back(collector); // build the branch and collect the modules whose initialization is deferred until the // whole chain (including the aggregator connection) is wired std::vector modulesToInitialize; @@ -71,6 +64,15 @@ int DynamicClassifier::createBranch() aggregator->setGateSize("in", aggregator->gateSize("in") + 1); cGate *aggregatorInputGate = aggregator->gate("in", aggregator->gateSize("in") - 1); branchOutputGate->connectTo(aggregatorInputGate); + // the sink references resolve the far end of the path eagerly, so they can only be taken + // now that the whole branch, up to and including the aggregator, is connected + outputGates.push_back(classifierOutputGate); + PassivePacketSinkRef consumer; + consumer.reference(classifierOutputGate, false); + consumers.push_back(consumer); + ActivePacketSinkRef collector; + collector.reference(classifierOutputGate, false); + collectors.push_back(collector); for (auto module : modulesToInitialize) module->callInitialize(); return index; From 680d9c8a13e734ce3f122841c26065ed75ff0b05 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 10 Aug 2026 15:55:37 +0200 Subject: [PATCH 5/8] queueing: never shrink the branch submodule vector in DynamicClassifier 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) --- src/inet/queueing/classifier/DynamicClassifier.cc | 13 +++++++++++-- src/inet/queueing/classifier/DynamicClassifier.h | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index 4dd8b54bb78..ba66938014f 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -78,6 +78,15 @@ int DynamicClassifier::createBranch() return index; } +void DynamicClassifier::growSubmoduleVector(cModule *parent, const char *name, int size) +{ + // the vector is only ever extended: it may have been declared larger in NED, and shrinking + // one that still holds submodules is an error + if (!parent->hasSubmoduleVector(name)) + throw cRuntimeError("The submodule vector '%s' is missing from %s", name, parent->getFullPath().c_str()); + parent->setSubmoduleVectorSize(name, std::max(parent->getSubmoduleVectorSize(name), size)); +} + void DynamicClassifier::forwardMatchingParams(cModule *module) { // copy the value of every parameter the created module shares (by name) with this @@ -94,7 +103,7 @@ void DynamicClassifier::forwardMatchingParams(cModule *module) cGate *DynamicClassifier::createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize) { cModule *parent = getParentModule(); - parent->setSubmoduleVectorSize(submoduleName, index + 1); + growSubmoduleVector(parent, submoduleName, index + 1); cModule *module = moduleType->create(submoduleName, parent, index); classifierOutputGate->connectTo(module->gate("in")); module->finalizeParameters(); @@ -131,7 +140,7 @@ cGate *DynamicClassifier::spliceBranch(int index, cGate *classifierOutputGate, s for (size_t i = 0; i < chain.size(); i++) { cModule *module = chain[i]; module->setName(("splicetmp_" + names[i]).c_str()); - parent->setSubmoduleVectorSize(names[i].c_str(), index + 1); + growSubmoduleVector(parent, names[i].c_str(), index + 1); module->changeParentTo(parent); module->setNameAndIndex(names[i].c_str(), index); modulesToInitialize.push_back(module); diff --git a/src/inet/queueing/classifier/DynamicClassifier.h b/src/inet/queueing/classifier/DynamicClassifier.h index f8ec854dc6b..162469f389a 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.h +++ b/src/inet/queueing/classifier/DynamicClassifier.h @@ -50,6 +50,7 @@ class INET_API DynamicClassifier : public PacketClassifier virtual int classifyPacket(Packet *packet) override; virtual int createBranch(); + virtual void growSubmoduleVector(cModule *parent, const char *name, int size); virtual void forwardMatchingParams(cModule *module); virtual cGate *createModuleBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); virtual cGate *spliceBranch(int index, cGate *classifierOutputGate, std::vector& modulesToInitialize); From 21fc255974edbbfd323f456b216a4e4619a800be Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 11:28:11 +0200 Subject: [PATCH 6/8] queueing: record spliced branch statistics under the final module path The submodules DynamicClassifier splices into a branch are built inside the template compound, and only reparented and renamed afterwards. The result recorders of a module are created at the end of its buildInside(), however, and an output vector remembers the module's full path from the moment it is registered, so every branch recorded its vectors under the same ...splicetmp. path -- indistinguishable from each other. Scalars were unaffected, being recorded at finish() under the name the module has by then. Per-statistic configuration (recording modes, vector recording, recording intervals) was matched against the temporary path as well. Recreate the result recorders of each spliced submodule once it is at its final place, submodules first, so that a @statistic taking its source from a submodule signal is put back by the module that declares it. The discarded recorders take their output vectors with them, but a vector is declared in the result file when it is registered, so an empty declaration is left behind under the temporary name; the NED documentation says how to suppress those. --- .../queueing/classifier/DynamicClassifier.cc | 23 +++++++++++++++++++ .../queueing/classifier/DynamicClassifier.h | 1 + .../queueing/classifier/DynamicClassifier.ned | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/src/inet/queueing/classifier/DynamicClassifier.cc b/src/inet/queueing/classifier/DynamicClassifier.cc index ba66938014f..651477f9807 100644 --- a/src/inet/queueing/classifier/DynamicClassifier.cc +++ b/src/inet/queueing/classifier/DynamicClassifier.cc @@ -87,6 +87,28 @@ void DynamicClassifier::growSubmoduleVector(cModule *parent, const char *name, i parent->setSubmoduleVectorSize(name, std::max(parent->getSubmoduleVectorSize(name), size)); } +void DynamicClassifier::refreshResultRecorders(cModule *module) +{ + // The @statistic result recorders of a module are created while it is built, that is, while + // a spliced module is still inside the template compound, and an output vector remembers the + // module's full path from the moment it is registered. Left alone, every branch would record + // its vectors under the same ...