Autosave support#163
Autosave support#163AlexanderWells-diamond merged 53 commits intoDiamondLightSource:masterfrom jsouter:autosave
Conversation
|
Just tested with all the builder record types, looks like it's not happy with waveform record types (numpy array not serialisable to json), will either have to cast these to lists or try dumping to a yaml file, possibly with custom representers. Or use some sort of pickle-type approach, but probably preferable to be human readable. |
There was a problem hiding this comment.
Looks good overall, but I've raised a few questions about what exactly the API could/should be in various comments.
There's several bits that need doing:
- Tests; at least a couple of system-level tests, then as much unit testing as you feel appropriate. Any test that starts an IOC will need to use the
multiprocessingmechanism, as various parts of EPICS won't allow multiple IOC initializations in a single thread so a new one must be used for each IOC. A simple template is probablytest_record_wrapper_strintest_records.py. - Providing example code, either inside the existing
docs/examples/example_*.ioc.pyfile(s) or in a separate new one - Docs; Probably fine to make a new
how-todocument explaining how to configure and use autosave. - A
CHANGELOGentry - take a look at previous entries to see the format/syntax.
|
I have added support for autosaving non-VAL fields, which is done by calling EDIT: Have realised that alternatively we could force DISP=0 for In type records that require fields to be tracked in autosave. |
AlexanderWells-diamond
left a comment
There was a problem hiding this comment.
The overall code and structure seems pretty good now. Unfortunately there's a number of bits causing problems, and a handful of tasks that still need doing.
There's a lot of CI test failures, some of which are due to the Pipfile.lock change, and some of them to do with the autosave code.
For the Pipfile changes, there's a comment discussing whether they are needed or not.
For the other CI fails, I found some errors like this in a few places:
----------------------------- Captured stderr call -----------------------------
2024-07-16T09:07:11.552312123 WARN pvxs.tcp.setup Server unable to bind port 5075, falling back to [::]:39597
Process ForkServerProcess-5:
Traceback (most recent call last):
File "/opt/python/cp38-cp38/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
self.run()
File "/opt/python/cp38-cp38/lib/python3.8/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/project/tests/test_record_values.py", line 384, in run_ioc
builder.LoadDatabase()
File "/tmp/tmp.ZezuRo2hXU/venv/lib/python3.8/site-packages/softioc/builder.py", line 303, in LoadDatabase
autosave.load()
File "/tmp/tmp.ZezuRo2hXU/venv/lib/python3.8/site-packages/softioc/autosave.py", line 70, in load
Autosave._load()
File "/tmp/tmp.ZezuRo2hXU/venv/lib/python3.8/site-packages/softioc/autosave.py", line 186, in _load
cls._backup_sav_file()
File "/tmp/tmp.ZezuRo2hXU/venv/lib/python3.8/site-packages/softioc/autosave.py", line 117, in _backup_sav_file
sav_path = cls._get_current_sav_path()
File "/tmp/tmp.ZezuRo2hXU/venv/lib/python3.8/site-packages/softioc/autosave.py", line 139, in _get_current_sav_path
return cls.directory / f"{cls.device_name}.{SAV_SUFFIX}"
TypeError: unsupported operand type(s) for /: 'NoneType' and 'str'
So it seems there are some cases where the autosave code is being initialized with invalid values.
The extra bits of work that still need doing:
- Documentation.
api.rstneeds extending to include the new keywords added to record initialization, and then we probably also need a newhow-todoc to explain autosave and its mechanisms. - A CHANGELOG entry - copy a previous one and change the info, as the syntax is quite picky!
- Possibly a new example python IOC using autosave (or modifications to an existing one)
- At least one full-system IOC test that actually starts an IOC from a canned autosave file and proves it loads the new values. Feel free to ask me when you get to this task as it is quite complicated to make it work.
Araneidae
left a comment
There was a problem hiding this comment.
Looks good, have a few questions to look at.
| # Create records, set some of them to autosave, also save some of their fields | ||
|
|
||
| builder.aOut("AO", autosave=True) | ||
| builder.aIn("AI", autosave_fields=["PREC", "SCAN"]) |
There was a problem hiding this comment.
An interesting thought here: builder IN records must have .SCAN set to "I/O Intr" for proper operation (otherwise .set() won't work properly), so using "SCAN" as a saved field isn't the best example! I'd suggest either "EGU" or some of the alarm threshold fields.
softioc/autosave.py
Outdated
| self._last_saved_time = datetime.now() | ||
|
|
||
| @classmethod | ||
| def _backup_sav_file(cls): |
There was a problem hiding this comment.
Why not use __ prefix for local names? This is the normal Python style for member names, and it triggers automatic name mangling to hide the names from outside the class.
|
I will spend some time this afternoon looking back over original autosave and see if there's any features of that worth implementing. I'm not sure if there's any meaningful reason to try and implement things like the asVerify and the pass0/pass1 restoring of values before and after record initialization, but there are a few things like manual save restore that could be more relevant. I wonder if using the name autosave implies a sort of feature parity that we don't necessarily want |
|
One last feature request: It may well be useful to provide a context manager, that can automatically do Autosave for all PVs declared within it. Possibly also supporting a specific list of fields, something like this: And this will autosave |
I'll push fixes to the above comments soon, I've got lots of things in branches at the moment! The most straightforward way I could think to do this was to define a context manager in builder.py like this and have aOut etc call _set_user_defaults(fields). This is generic enough that it could be used for any EPICS field like EGU etc, would that be something we want? Or would it be better to rework the Autosave interface to work as a context manager? |
|
I would prefer to see the Autosave class used as the context manager. As we don't have to run any code during the creation of each PV, what we can do is track how many PVs were created during the lifetime of the context manager and then add them in bulk to the Autosave list. Something like this (untested) code: |
Got it, I think this specific implementation would be problematic because of circular imports, but I can see if I can come up with something that achieves the same thing. Of course could maybe avoid the import problem by importing within a class/function scope but could be a bit of a code smell. |
The simplest fix for that would be to replace from softioc.device_core import LookupRecordListwith from softioc import device_coreand invoke as |
|
I realise I was mistaken about how the device name work, I was under the impression that the device name gets preprended to PVs that were instantiated before Considering the idea of multiple devices within an ioc, would we want to support the ability to backup differnet groups of PVs to multiple files, each with a different save period? I have drafted this with both sync and async methods, and it is a feature provided by C++ autosave. |
|
I'm inclined to keep it simple, at least for the first release. If anyone wants the extra feature they can raise an issue for it. |
Araneidae
left a comment
There was a problem hiding this comment.
Looks good to me. Would like to discuss the Autosave context handler default arguments, but think this is fine.
AlexanderWells-diamond
left a comment
There was a problem hiding this comment.
Overall this is good, very close to done. I spotted a couple of potential bugs and there's a few docs changes that I'd like to see before this is finished.
tests/test_autosave.py
Outdated
| assert "MANUAL.EGU" in saved | ||
| assert "AUTOMATIC" in saved | ||
| assert "AUTOMATIC.PINI" in saved | ||
| assert "AUTOMATIC-OVERRIDDEN" in saved |
There was a problem hiding this comment.
Is it expected that the Context Manager's True would override the autosave=False? Or is there some other reason this item is in the saved list?
There was a problem hiding this comment.
That's how it works currently yes, I suppose we could find a way to use some True/False/None logic if we wanted an explicit calling of autosave=False to override the context manager?
There was a problem hiding this comment.
I see. My view would be that setting autosave=False on the record creation function should override everything else - it's the principle of least surprise to take its value over any other.
However, I can see that may be difficult to actually achieve. I can also see that it seems very unlikely anyone will ever actually try and do that. If there's an easy way to at least print/raise an error in this case then do that, otherwise it is a rather confusing behaviour. @Araneidae, any thoughts?
There was a problem hiding this comment.
Can roll back if required but just pushed something to address this: we call add_pv_to_autosave on all PVs at init now, and class variables are set in the context manager which are used to override the the arguments "autosave" and "autosave_fields" if not specified, so an explicit autosave=False will get applied. It also means we don't have to use LookupRecordList any more.
But I did realise that this newer implementation, and the LookupRecordList implementation would not be thread safe, in the sense that if a separate thread is creating PVs while another thread is creating PVs inside a context manager, we may accidentally track fields in autosave that we don't want to. I'm not sure if anyone would write their IOC that way, but may be something worth trying to guard against.
|
|
||
| @pytest.fixture | ||
| def existing_autosave_dir(tmp_path): | ||
| state = { |
There was a problem hiding this comment.
As this data is used in several tests, I'd be tempted to split it out into its own top level dictionary or a fixture, to make it obvious that these values will be references in many tests. Not required, just a nice to have.
|
I believe I have managed to make the context managers thread safe by using a singleton style class that inherits from threading.local. I have moved some of the context manager logic into a new _AutosaveContext class, but if this seems a bit too convoluted see the approach in the commit 2da2fbf which applies all the singleton/threading logic to Autosave. |
|
If we have a non-default list of autosave_fields for a PV inside an Autosave context manager, do we want to combine the lists or give priority to just the specified fields in the PV init? I suppose we still need to discuss if we want to track VAL inside autosave_fields instead of using autosave as its own kwarg. |
|
I'd be inclined to merge the list of extra fields. If users want to have an override they can just remove that one PV from the context manager. Regarding tracking VAL, it's an awkward one due to the assumption that a record name without |
…ing other test files
reset device name between tests
stop autosave IOC test from running forever
|
Great! I don't seem to have permissions to merge but happy to have it squashed with a message like "add support for saving and restoring pv fields with autosave" |
It's possible a mistake was made in #163 that means we no longer download all artifacts into one directory...
An after-effect of PR #163 means that artifacts now have unique names per job. We have to ensure we download all of them to release all wheels.
Addressing #162
Adding in support for EPICS autosave style periodic saving of PV values and fields to a yaml-based backup file, from which initial values can be loaded on a restart.