DiamondLightSource/pythonSoftIOC

Support for autosaving PV values

Closed this issue · 11 comments

I have been working on an IOC to track the placement of samples on a carousel. It would be convenient if the readback values for the sample names at various positions would persist after an IOC restart in case of a power off event (or anything else prompting an IOC restart) so that they don't have to be manually scanned/written back in. I could rewrite as a normal C++ asyn IOC or implement my own autosave by writing to the filesystem but more official autosave support would be helpful.

MX would also find this useful. We have a use-case where we'd like to have a baton between GDA and Bluesky software, which is ran on a Python IOC. An autosave would be needed so it remembers who is in control after crashing / needing a restart.

@jsouter I think my preferred approach would be a pure python interface like I07's. Can you think of a way to make it generic so it could be put into pythonSoftIOC?

I'll have a look at what's being done on i07 and see what I can come up with

Drafting this here: https://github.com/jsouter/pythonSoftIOC/tree/autosave
Currently you have to instantiate the Autosave class and pass to it a list of PVs you want to save, trying to inspect if there's a good way to use some builder magic to get a list of the records so that we could use .req files instead and look for PVs globally by name, though maybe explicitly passing the PV objects (RecordWrappers, technically) to Autosave is preferable. Right now the onus on the IOC developer to periodically call save in their main dispatcher loop, though it could be reworked so that there's always an autosave loop running in the background that periodically calls save if the save_period has elapsed and any of the PVs have changed.

I suggest two things:

  • Use an extra builder argument (maybe autosave=True) to automatically mark PVs for autosaving
  • Run a background thread to periodically check for changes

It might be worth having a look at how epics_device does things. However, this is written in C and so may be a less accessible reference.

I echo Michael's advice of using another flag during the build process to mark PVs for autosaving. Seems much cleaner than trying to maintain a separate list of PVs to be saved.

Although with the mention of .req files, this sounds like you have an alternative idea of how the interface should work. Is that file the one that is used by the EPICS autosave feature?

P.S. You can inspect the list of created records by calling softioc.device_core.LookupRecordList()

Cheers both, just noticed builder.LookupRecord() which should solve that problem. I think the background thread/builder argument approach makes sense so I will have a go at that.
.req files are what the regular autosave module uses, just acts as a list of PVs that should be saved/loaded, I have no real preference for doing it this way just wondered if people thought it better to pass it in as configuration/make it look more like the C/C++ version.

Okay, have a version of that here https://github.com/jsouter/pythonSoftIOC/tree/autosave, the autosave thread gets kicked off during the __init__for CothreadDispatcher/AsyncioDispatcher. I believe this works as expected but I will need to do some work to figure out where the thread should be started and how to handle its shutdown.

The directory must be set by calling softioc.autosave.configure as below

from softioc import softioc, builder
from softioc.asyncio_dispatcher import AsyncioDispatcher
import asyncio
from softioc import autosave
import time

DEVICE_NAME = "MY-SOFT-IOC-01"


builder.SetDeviceName(DEVICE_NAME)
autosave.configure(directory=f"/tmp/data/{DEVICE_NAME}", save_period = 5)
saved_ao = builder.aOut('SAVEME', autosave=True)
unsaved_ao = builder.aOut('DONTSAVEME', autosave=False)

async def main():
    i = saved_ao.get()
    j = unsaved_ao.get()
    while True:
        i += 1
        j += 1
        await asyncio.sleep(1)
        saved_ao.set(i)
        unsaved_ao.set(j)
        print(saved_ao.get(), unsaved_ao.get())

builder.LoadDatabase()
dispatcher = AsyncioDispatcher()
softioc.iocInit(dispatcher(main))
softioc.interactive_ioc(globals())

The autosave directory could be passed in with argparse or read from a config file

That looks like a good initial go at an implementations, it's probably time to open a PR so we can give more precise feedback.

Where the autosave directory comes from is immaterial to pythonSoftIoc itself, it'd be dependant on how exactly the users want to load that data.

Rather than walking the complete list of PVs on every autosave cycle you could instead maintain a list of PVs that are marked with the autosave flag and add PVs to this list (by calling into the autosave module) when you see the autosave flag set.

On that note, you should be able to push the resulting if kargs.pop('autosave', False): autosave.add_pv(self) call up to the common ProcessDeviceSupportCore class rather than repeating it for each subclass. (I wouldn't push it any higher up the hierearchy, though!)

Completed as of #163