How to add a Placement Strategy
Operator placement decides on which worker each logical operator runs.
It is separate from source and sink placement: sources and sinks already carry their configured Host, while a placement strategy assigns a PlacementTrait to every logical operator in between.
Downstream distributed-planning code consumes these traits to create per-worker fragments and network edges.
BottomUpOperatorPlacer is the current placement implementation and the best reference when adding a new placement strategy.
1. Overview
Placement code lives in nes-query-optimizer:
nes-query-optimizer/
├── include/Placement/
│ ├── BottomUpPlacement.hpp
│ ├── QueryDecomposition.hpp
│ └── MyPlacement.hpp
└── src/Placement/
├── BottomUpPlacement.cpp
├── QueryDecomposition.cpp
├── MyPlacement.cpp
└── CMakeLists.txt
A placement strategy should follow the shape of BottomUpOperatorPlacer:
class MyOperatorPlacer final
{
SharedPtr<const WorkerCatalog> workerCatalog;
public:
explicit MyOperatorPlacer(SharedPtr<const WorkerCatalog> workerCatalog) : workerCatalog(std::move(workerCatalog)) { }
void apply(LogicalPlan& logicalPlan);
};
apply mutates the LogicalPlan by replacing it with an equivalent plan whose operators all have a PlacementTrait.
The placement strategy must not insert network operators itself; that happens after placement.
2. Dependencies & CMake
Add the new implementation file to nes-query-optimizer/src/Placement/CMakeLists.txt:
add_source_files(nes-query-optimizer
BottomUpPlacement.cpp
QueryDecomposition.cpp
MyPlacement.cpp
)
The placement strategy becomes part of the nes-query-optimizer library.
There is currently no placement-strategy plugin registry, so a new strategy must be wired into OperatorPlacer explicitly.
If the strategy needs an additional dependency, add the find_package call and target_link_libraries entry in nes-query-optimizer/CMakeLists.txt.
BottomUpOperatorPlacer is an example: it uses HiGHS and nes-query-optimizer links highs::highs privately.
3. Selection & Configuration
OperatorPlacer::place is the placement integration point.
It currently selects the placement strategy by directly constructing BottomUpOperatorPlacer.
For a one-off replacement, include the new placement header in nes-query-optimizer/src/Phases/OperatorPlacer.cpp and call the new placer where BottomUpOperatorPlacer is invoked.
For a selectable strategy, add an enum and an EnumOption to nes-query-optimizer/interface/QueryOptimizerConfiguration.hpp:
enum class OperatorPlacementStrategy : uint8_t
{
BOTTOM_UP,
MY_STRATEGY
};
EnumOption<OperatorPlacementStrategy> placementStrategy
= {"placement_strategy",
OperatorPlacementStrategy::BOTTOM_UP,
"Operator placement strategy [BOTTOM_UP|MY_STRATEGY]."};
std::vector<BaseOption*> getOptions() override { return {&joinStrategy, &placementStrategy, &network}; }:
Configuration enum values are parsed by name, so the example strategy can be selected with placement_strategy=MY_STRATEGY.
Then select the strategy in OperatorPlacer::place before query decomposition:
switch (defaultQueryOptimization.placementStrategy)
{
case OperatorPlacementStrategy::BOTTOM_UP:
BottomUpOperatorPlacer(copyPtr(workerCatalog)).apply(plan);
break;
case OperatorPlacementStrategy::MY_STRATEGY:
MyOperatorPlacer(copyPtr(workerCatalog)).apply(plan);
break;
}
The strategy must run at the placement integration point, because downstream distributed-planning code expects every operator to have a PlacementTrait.
4. Implementation
A placement strategy receives:
- the
LogicalPlanhanded to placement - the
WorkerCatalog - the current
NetworkTopologyviaworkerCatalog->getTopology() - worker capacities via
workerCatalog->getWorker(host).value().maxOperators - fixed source hosts via
SourceDescriptorLogicalOperator::getSourceDescriptor().getHost() - fixed sink hosts via
SinkLogicalOperator::getSinkDescriptor()->getHost()
A placement strategy must produce:
- exactly one worker assignment for every logical operator in the plan
- a
PlacementTraiton every logical operator - a plan with the same logical structure, enriched with placement traits, so downstream distributed planning can process it
Recommended behavior:
- Validate that all source and sink hosts exist in the topology.
- Validate that the topology connects every placed source to the placed sink.
- Keep source operators on their source descriptor host.
- Keep the sink operator on its sink descriptor host.
- Respect
maxOperatorsif the strategy models worker capacity. - Throw
PlacementFailurefor user-facing infeasibility. - Use
PRECONDITIONorINVARIANTfor internal bugs.
BottomUpOperatorPlacer follows by building a placement map from OperatorId to NetworkTopology::NodeId, then recursively attaching PlacementTrait to every operator:
LogicalOperator addPlacementTrait(const LogicalOperator& op, const std::unordered_map<OperatorId, NetworkTopology::NodeId>& placement)
{
auto oldTraitSet = op.getTraitSet();
USED_IN_DEBUG auto addedTrait = oldTraitSet.tryInsert(PlacementTrait(placement.at(op.getId())));
INVARIANT(addedTrait, "There should not have been a placement trait");
return op.withTraitSet(oldTraitSet)
.withChildren(
op.getChildren()
| std::views::transform(
[&placement](const LogicalOperator& child) -> LogicalOperator { return addPlacementTrait(child, placement); })
| std::ranges::to<std::vector>());
}
Use the same immutable-plan style: create updated operators with withTraitSet and withChildren, then replace the plan root:
logicalPlan = LogicalPlan(logicalPlan.getQueryId(), {addPlacementTrait(logicalPlan.getRootOperators().front(), placement)});
Example Strategy Shape
A simple strategy can be structured as:
- Read a topology snapshot from the worker catalog.
- Validate source and sink hosts.
- Iterate over the plan with
BFSRange(logicalPlan.getRootOperators().front()). - Choose a
NetworkTopology::NodeIdfor each operator. - Attach placement traits recursively.
5. Testing
Test both the strategy-specific placement decisions and the distributed plan produced after placement.
Useful existing references are in nes-frontend/tests/DistributedPlanningTest.cpp.