Skip to content

Commit a94dd67

Browse files
committed
Fix Tier 1 code review findings
- Make LoadError 2-arg constructor private (was reading moved-from state) - Remove mutable instances() cache (data race risk under shared lock) - Add ObserverGuard RAII class to prevent dangling observer pointers - Document add_plugin permissive dep policy and subscription ID invariant
1 parent 5382ae0 commit a94dd67

3 files changed

Lines changed: 85 additions & 32 deletions

File tree

plugin_arch/PluginError.hpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,9 @@ class PluginError : public std::runtime_error {
3131
};
3232

3333
// dlopen / LoadLibrary failed. Carries the platform-specific error string.
34+
// Always construct via ::make() to avoid reading moved-from state.
3435
class LoadError : public PluginError {
3536
public:
36-
LoadError(std::string library_path, std::string platform_error)
37-
: PluginError(std::move(library_path),
38-
"Failed to load library: " + this->library_path() +
39-
" (" + platform_error + ")"),
40-
platform_error_(std::move(platform_error)) {}
41-
42-
// Two-phase construction: we need library_path stored before building what().
43-
// Use a static factory to build the message cleanly.
4437
static LoadError make(std::string library_path, std::string platform_error) {
4538
std::string msg = "Failed to load library: " + library_path +
4639
" (" + platform_error + ")";

plugin_arch/PluginManager.hpp

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ class PluginManager {
332332
infos[result.info_idx].deps});
333333
rebuild_name_index();
334334
wire_instance_tracked(plugins_.back(), config_map);
335-
instances_dirty_ = true;
335+
336336
}
337337
}
338338
}
@@ -377,26 +377,30 @@ class PluginManager {
377377
}
378378

379379
// --- Dependency + version checks (D6) ---
380+
// add_plugin is permissive about missing deps (provider may be added
381+
// later — the reverse version check on line ~407 catches mismatches
382+
// when the provider arrives). Version constraints are still validated
383+
// when the provider is already loaded.
380384
std::vector<std::string> deps;
381385
auto* dep_aware = dynamic_cast<IDependencyAware*>(instance.get());
382386
if (dep_aware) {
383387
for (const auto& raw_dep : dep_aware->dependencies()) {
384388
auto parsed = Dependency::parse(raw_dep);
385389
deps.push_back(parsed.type);
386390

387-
if (parsed.op != Dependency::Op::any) {
388-
// Find the provider and check version constraint.
389-
for (const auto& lp : plugins_) {
390-
if (lp.entry.type == parsed.type) {
391+
// Check version constraint only if the provider is already loaded.
392+
for (const auto& lp : plugins_) {
393+
if (lp.entry.type == parsed.type) {
394+
if (parsed.op != Dependency::Op::any) {
391395
auto provider_version = SemVer::parse(lp.entry.version);
392396
if (!parsed.satisfied_by(provider_version)) {
393397
throw std::runtime_error(
394398
"Plugin '" + entry.name + "' requires " + raw_dep +
395399
" but '" + lp.entry.name + "' provides version " +
396400
lp.entry.version);
397401
}
398-
break;
399402
}
403+
break;
400404
}
401405
}
402406
}
@@ -448,7 +452,7 @@ class PluginManager {
448452
rebuild_name_index();
449453
wire_instance_tracked(plugins_.back(), config_map, /*skip_validation=*/true);
450454
notify_loaded(entry.name, entry.type);
451-
instances_dirty_ = true;
455+
452456
}
453457

454458
// --- Per-plugin unload (A1) ---
@@ -495,7 +499,7 @@ class PluginManager {
495499
plugins_ = std::move(remaining);
496500
rebuild_name_index();
497501
rebuild_locator();
498-
instances_dirty_ = true;
502+
499503
}
500504

501505
// --- Per-plugin reload (A1) ---
@@ -568,7 +572,7 @@ class PluginManager {
568572
}
569573

570574
notify_reloaded(name, target_type);
571-
instances_dirty_ = true;
575+
572576
}
573577

574578
// --- Health checks (B1) ---
@@ -793,7 +797,7 @@ class PluginManager {
793797
locator_.clear();
794798
event_bus_.clear();
795799
load_errors_.clear();
796-
instances_dirty_ = true;
800+
797801
}
798802

799803
[[nodiscard]] ServiceLocator& locator() { return locator_; }
@@ -802,17 +806,13 @@ class PluginManager {
802806
[[nodiscard]] EventBus& event_bus() { return event_bus_; }
803807
[[nodiscard]] const EventBus& event_bus() const { return event_bus_; }
804808

805-
[[nodiscard]] const std::vector<std::shared_ptr<IPlugin>>& instances()
806-
const {
807-
if (instances_dirty_) {
808-
instances_cache_.clear();
809-
instances_cache_.reserve(plugins_.size());
810-
for (const auto& lp : plugins_) {
811-
instances_cache_.push_back(lp.instance);
812-
}
813-
instances_dirty_ = false;
809+
[[nodiscard]] std::vector<std::shared_ptr<IPlugin>> instances() const {
810+
std::vector<std::shared_ptr<IPlugin>> result;
811+
result.reserve(plugins_.size());
812+
for (const auto& lp : plugins_) {
813+
result.push_back(lp.instance);
814814
}
815-
return instances_cache_;
815+
return result;
816816
}
817817

818818
// Get a loaded plugin by name. Returns nullptr if not found.
@@ -859,8 +859,6 @@ class PluginManager {
859859
EventBus event_bus_;
860860
std::vector<LoadedPlugin> plugins_;
861861
std::unordered_map<std::string, std::size_t> name_index_;
862-
mutable std::vector<std::shared_ptr<IPlugin>> instances_cache_;
863-
mutable bool instances_dirty_ = true;
864862
std::vector<ErrorRecord> load_errors_;
865863
std::vector<PluginObserver*> observers_;
866864

@@ -1148,7 +1146,7 @@ class PluginManager {
11481146
rebuild_name_index();
11491147
wire_instance_tracked(plugins_.back(), config_map);
11501148
notify_loaded(entry.name, entry.type);
1151-
instances_dirty_ = true;
1149+
11521150
}
11531151

11541152
// Wire all opt-in mixins on an instance.
@@ -1218,18 +1216,43 @@ class PluginManager {
12181216
}
12191217

12201218
// Wire + track EventBus subscription IDs (for enable/disable).
1219+
//
1220+
// Relies on EventBus::next_id_ being monotonically increasing with no
1221+
// reuse: all IDs allocated between [before, after) belong to this plugin.
1222+
// This holds because PluginManager is single-threaded (or exclusively
1223+
// locked via ThreadSafe), and wire_instance may call on_init() /
1224+
// set_event_bus() which subscribe — those subscriptions correctly belong
1225+
// to this plugin. Unsubscribes during wiring don't break the invariant
1226+
// because IDs are never recycled.
12211227
void wire_instance_tracked(LoadedPlugin& lp, const ConfigMap& config_map,
12221228
bool skip_validation = false) {
12231229
auto before = event_bus_.next_subscription_id();
12241230
wire_instance(lp.instance, lp.entry, config_map, skip_validation);
12251231
auto after = event_bus_.next_subscription_id();
12261232

1227-
// Record all subscription IDs created during wiring
12281233
lp.subscription_ids.clear();
12291234
for (auto id = before; id < after; ++id) {
12301235
lp.subscription_ids.push_back(id);
12311236
}
12321237
}
12331238
};
12341239

1240+
// --- ObserverGuard inline implementation ---
1241+
// Defined here because it needs PluginManager to be complete.
1242+
1243+
inline ObserverGuard::ObserverGuard(PluginManager& manager,
1244+
PluginObserver* observer)
1245+
: manager_(&manager), observer_(observer) {
1246+
if (observer_) manager_->add_observer(observer_);
1247+
}
1248+
1249+
inline ObserverGuard::~ObserverGuard() { release(); }
1250+
1251+
inline void ObserverGuard::release() {
1252+
if (observer_) {
1253+
manager_->remove_observer(observer_);
1254+
observer_ = nullptr;
1255+
}
1256+
}
1257+
12351258
} // namespace plugin_arch

plugin_arch/PluginObserver.hpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
// Hosts register observers with PluginManager to receive callbacks
44
// when plugins are loaded, unloaded, reloaded, enabled, or disabled.
55
// Useful for metrics, logging, and debugging.
6+
//
7+
// Use ObserverGuard for RAII auto-deregistration to prevent dangling pointers.
68

79
#pragma once
810

911
#include <string>
1012

1113
namespace plugin_arch {
1214

15+
class PluginManager;
16+
1317
class PluginObserver {
1418
public:
1519
virtual ~PluginObserver() = default;
@@ -26,4 +30,37 @@ class PluginObserver {
2630
const std::string& type) {}
2731
};
2832

33+
// RAII guard that auto-deregisters the observer on destruction.
34+
// Prevents dangling pointer callbacks when the observer is destroyed
35+
// before the PluginManager.
36+
class ObserverGuard {
37+
public:
38+
ObserverGuard(PluginManager& manager, PluginObserver* observer);
39+
~ObserverGuard();
40+
41+
ObserverGuard(const ObserverGuard&) = delete;
42+
ObserverGuard& operator=(const ObserverGuard&) = delete;
43+
44+
ObserverGuard(ObserverGuard&& other) noexcept
45+
: manager_(other.manager_), observer_(other.observer_) {
46+
other.observer_ = nullptr;
47+
}
48+
49+
ObserverGuard& operator=(ObserverGuard&& other) noexcept {
50+
if (this != &other) {
51+
release();
52+
manager_ = other.manager_;
53+
observer_ = other.observer_;
54+
other.observer_ = nullptr;
55+
}
56+
return *this;
57+
}
58+
59+
void release();
60+
61+
private:
62+
PluginManager* manager_;
63+
PluginObserver* observer_;
64+
};
65+
2966
} // namespace plugin_arch

0 commit comments

Comments
 (0)