-
Notifications
You must be signed in to change notification settings - Fork 71
feat(retain): retain-variable persistence for runtime v4 [NODE-94] #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
83a3cc3
ab55674
1378bc2
cf912ef
c56f517
588c7bc
ed19e22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| v4.1.10 | ||
| v4.2.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1446,6 +1446,14 @@ int native_plugin_get_symbols(plugin_instance_t *plugin) | |
| // get_stats is fully optional — plugins that don't publish statistics | ||
| // simply don't export it. No warning. | ||
|
|
||
| // Retain store (NODE-94), fully optional. A plugin exporting both becomes | ||
| // a candidate for this device's retain store; see | ||
| // plugin_driver_find_retain_store. No warning when absent — most plugins | ||
| // have nothing to do with retention. | ||
| native_bundle->retain_save = (plugin_retain_save_func_t)dlsym(handle, "retain_save"); | ||
| native_bundle->retain_load = (plugin_retain_load_func_t)dlsym(handle, "retain_load"); | ||
| native_bundle->retain_clear = (plugin_retain_clear_func_t)dlsym(handle, "retain_clear"); | ||
|
|
||
| // Store the native bundle and handle in the plugin instance | ||
| plugin->native_plugin = native_bundle; | ||
|
|
||
|
|
@@ -1473,6 +1481,79 @@ void python_plugin_cycle(plugin_instance_t *plugin) | |
| // Call cycle_start for all active native plugins that have registered the hook | ||
| // This should be called at the beginning of each PLC scan cycle, before PLC logic execution | ||
| // Plugins opt-in by implementing cycle_start(); opt-out by not implementing it (NULL pointer) | ||
| // --------------------------------------------------------------------------- | ||
| // Retain store | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| static bool plugin_provides_retain_store(const plugin_instance_t *p) | ||
| { | ||
| if (!p) return false; | ||
|
|
||
| // A DISABLED plugin is not a store, even though its symbols resolved. | ||
| // | ||
| // Loading resolves symbols for every plugin in plugins.conf; only starting | ||
| // is gated on `enabled`. Without this check a disabled plugin is still | ||
| // picked as the store, so retain reports itself active, hands it the blob | ||
| // every scan, and gets nothing back on the next boot — the values are | ||
| // simply gone, with a log line at start saying retain is configured and | ||
| // working. Found on hardware: an upload rewrote plugins.conf, disabled the | ||
| // storage plugin, and retain went on claiming to work. | ||
| if (!p->config.enabled) return false; | ||
|
|
||
| // BOTH halves required. A store that can save and not load is worse than | ||
| // none: it would accept values every scan and silently never give them | ||
| // back, which looks like working retention right up until the reboot that | ||
| // matters. | ||
| return p->native_plugin && p->native_plugin->retain_save && p->native_plugin->retain_load; | ||
| } | ||
|
|
||
| plugin_instance_t *plugin_driver_find_retain_store(plugin_driver_t *driver) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ Testing — no coverage for the new retain-store selection or clear-on-upload behavior This PR adds four new public functions here ( Suggestion: at minimum, add cases to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partly addressed in #178, and I want to be straight about the part I declined. Added: 14 pytest cases for Not added: the cases in So the C-side selection logic you name — first-store-wins, disabled-is-not-a-store, half-a-store-is-not-a-store — is currently proven only on hardware. That is a real hole and your instinct about it is right; the fix is giving that harness a build, which is its own piece of work. Happy to take it if you want it in this cycle. |
||
| { | ||
| if (!driver) return NULL; | ||
|
|
||
| plugin_instance_t *chosen = NULL; | ||
| for (int i = 0; i < driver->plugin_count; i++) | ||
| { | ||
| plugin_instance_t *p = &driver->plugins[i]; | ||
| if (p->degraded || !plugin_provides_retain_store(p)) continue; | ||
|
|
||
| if (!chosen) | ||
| { | ||
| chosen = p; | ||
| continue; | ||
| } | ||
| // Two stores would both appear to work and disagree on the next boot, | ||
| // which is a worse failure than refusing the second. First wins, and | ||
| // the rest are named so the misconfiguration is visible. | ||
| log_warn("Retain: plugin '%s' also provides retain storage; ignoring it — " | ||
| "'%s' was found first", | ||
| p->config.name, chosen->config.name); | ||
| } | ||
| return chosen; | ||
| } | ||
|
|
||
| int plugin_driver_retain_save(plugin_instance_t *store, const uint8_t *blob, uint16_t len) | ||
| { | ||
| if (!plugin_provides_retain_store(store)) return -1; | ||
| return store->native_plugin->retain_save(blob, len); | ||
| } | ||
|
|
||
| int plugin_driver_retain_load(plugin_instance_t *store, uint8_t *out, uint16_t cap, uint16_t *out_len) | ||
| { | ||
| if (out_len) *out_len = 0; | ||
| if (!plugin_provides_retain_store(store)) return -1; | ||
| return store->native_plugin->retain_load(out, cap, out_len); | ||
| } | ||
|
|
||
| int plugin_driver_retain_clear(plugin_instance_t *store) | ||
| { | ||
| // Optional third hook: a plugin without it cannot be cold-reset, and | ||
| // reporting that as failure would make the editor's post-upload clear look | ||
| // broken on every such device. | ||
| if (!store || !store->native_plugin || !store->native_plugin->retain_clear) return 0; | ||
| return store->native_plugin->retain_clear(); | ||
| } | ||
|
|
||
| void plugin_driver_cycle_start(plugin_driver_t *driver) | ||
| { | ||
| if (!driver || driver->plugin_count == 0) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.