Update settings handling; add provenance and allow archiving unused settings - #623
Draft
dale-wahl wants to merge 27 commits into
Draft
Update settings handling; add provenance and allow archiving unused settings#623dale-wahl wants to merge 27 commits into
dale-wahl wants to merge 27 commits into
Conversation
… written by backend
…ther way to notify on settings audit
…e provenance of migrate's archived settings
…self); move submenus to config_definitions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
4CAT's
settingstable has no record of where a setting came from, so settings left behind by renames and removed features accumulate forever and show up in the settings panel as junk. They could not be cleaned up safely, because an undeclared setting is indistinguishable from one whose extension is merely uninstalled, disabled, or failing to import.This records which module declares each setting, uses that to tell those cases apart, and gives us a way to remove the ones that really are gone — reversibly.
Why not the
extension.prefixThe issue proposed prefixing extension settings so they could be identified. I went a different way, for two reasons.
Renaming a setting like
selenium.browsermeans changing everyconfig.get()call, everyrequired_settingsentry and everyconfigblock at the same time. Any extension not updated would back to defaults and we would lose the value, not just the row.Additionally, a prefix says a setting is extension-owned. It cannot say whether the owner is absent because it was uninstalled, switched off, or crashed on import — and that last case applies to core processors too, so anything keyed on "is this an extension?" would miss that.
For collision-proofing, new settings should go under the extension's own id — my_extension.api_key and so on. Not extensions..: that is one of the namespaces reserved for core (see below), so anything declared under it is refused. The extensions README.md says so now.
How all this works
ModuleCollectornow records which worker declared each setting instead of flattening them all together. That goes into a newconfig/module_config_provenance.binalongside the existingmodule_config.bin, which keeps its shape unchanged because every reader merges it straight into the config definition — including a front-end container that may still be running older code. I kept the two files separate on purpose because of that (old code will never touch the new file).After the modules load, the back-end writes this into
settings_declarations(common/config_manager.py,backend/bootstrap.py). Every stored setting can then be sorted into one of five states, only one of which is ever offered for removal:Age is measured against the last start-up on which every module imported, not against the clock (
4cat.declarations_last_clean_scan). While an import is broken that marker stops advancing, so nothing can age into looking removed just because 4CAT currently cannot see it (mostly in case we ship something broken and do not notice it immediately).Age is measured from the first complete start-up that found a setting missing, recorded per setting in settings_declarations.absent_since. There is a last seen, but the gap between two start-ups is however long the server happened to be up, so measuring from that would make anything an upgrade removes look long gone the moment it went. absent_since is only ever written on a start-up where every module imported 4cat.declarations_last_scan_complete), so a broken import still cannot age anything out.
A daily worker reports what it finds to admins (
backend/workers/audit_settings.py), behind4cat.report_orphan_settings, off by default — on a server nobody is developing against there is nothing to act on. It remembers which set it last reported in 4cat.declarations_reported, so dismissing the notification does not bring it back tomorrow. I discovered canonical_id means "came from the phone-home server" so could not be used and I had to match the notification text itself (this might be worth an improvement on the notifications table).Undeclared settings are no longer rendered in the settings panel at all. A line at the foot of the page links to a new Unused settings page (
webtool/templates/controlpanel/settings-unused.html) listing them with why each is kept, and an Archive button for the ones that qualify.Nothing is destroyed
Archiving moves a setting's values — the global one and every tag — into
settings_archive, and they can be restored from the same page. The guard lives inConfigManager.archive_setting(), not in the view, so a request naming a setting directly cannot get past it. We could allow archiving other settings too if we want.Two related fixes fell out of this:
A module could previously override a core setting's definition, because module config was merged over core with a plain
dict.update(). That is now refused rather than merged, and namespaces belonging to core (privileges.,flask.,4cat.,path.,datasources.,extensions.,logging.) are refused outright so an extension cannot claim a name a later 4CAT version might use. This matters because a definition controls a setting'sglobalflag — a module setting that on a core privilege would have made every per-tag restriction silently stop applying. Probably not a risk so much as a mistake (installing an extension by nature has lots of risks anyway).module_config.binis now written atomically. It was truncated and rewritten in place, which a reader in the other container could catch mid-write; the retry that was meant to survive that but probably would have failed, because it re-read the same file handle after a failed unpickle. This should all but avoid that happening (I think it could still try to read right as a file was being replaced so I left the retry).Needs an upgrade for the database
VERSIONgoes to 1.57.migrate-1.56-1.57.pycreates the two tables and archives 18 core settings for features 4CAT no longer has — renames, the Reddit datasource, the removed scheduler, the old per-processor proxy settings, and so on. he list is written out in the migration with a note on what became of each one, since none of it can be figured out automatically. Now that won't be a problem!Extension settings are not touched, including ones an extension has renamed itself. That is the extension author's call, not ours, and an older version of it may still be in use somewhere. I am not sure if or how we could encourage extensions to remove old settings, but we can track them now so perhaps with versioning of extensions.
On the my DevCAT I tested against: 182 settings attributed, 27 undeclared, 18 archived, 9 deliberately left (extensions and future feature settings).
Two things found but not fixed here
api.youtube.keywas declared in bothconfig_definition.pyandyoutube_metadata.py. Since module config was merged after core, the processor's copy had been silently winning. Its help text is now the one in the core declaration sonothing changes in the interface, and the duplicate is gone.
api.openai.api_keyis read inprocessors/machine_learning/audio_to_text.pybut declared nowhere, so it cannot be setfrom the interface and the OpenAI Whisper fallback never fires. Left alone here — it either wants re-declaring or retiring in favor of the LLM settings. Probably LLM, but need to sort out what that looks like. Out of this PR anyway.