Fork me on GitHub
#babashka
<
2024-03-02
>
Omer ZAK01:03:57

I am developing a Babashka script for mounting and unmounting encrypted LVM partitions in a hot-plugged disk drive (such as a DiskOnKey which has been formatted, under Linux, to have encrypted LVM partitions). To mount, need to perform a series of operations: 1. Verify the desired mount point. 2. cryptSetup luksOpen to deal with the encrypted partition. 3. Wait until the physical LVM volume and the LVM volume group are recognized. 4. Enable the logical LVM volume (using lvchange -ay) 5. Actually mount the logical LVM volume on the mount point. If all operations are successful, I want the script to exit and let the user work with the files in the logical LVM volume. Eventually, the user will run another script to unmount the logical LVM volume and undo all the other operations. The usual mechanism of nested try-finally clauses (in which a finally clause undoes the action of the corresponding try) is not suitable for this use case, because it assumes that the user wants to undo in reverse order all successful operations immediately after the last mount is successful. In other words, it assumes that the user wants to do his work inside the script, after successfully mounting the logical LVM volume (step 5 above); then once done, immediately unmount the logical LVM volume and doing the other steps in reverse, ending with cryptSetup luksClose (and maybe deleting the mount point). However, I want to have the script undo successful operations in reverse order only if any operation failed. In other words, skip the contents of all finally clauses if all operations were successful. In Python, I'd set a flag when all operations are successful, and add a check for this flag in all finally clauses. However, it is too ugly for Clojure. Is there any idiomatic way to express this in Clojure?

Bob B02:03:52

it sounds like an answer might be to put the undos in the catch clauses, and throw if something doesn't succeed

Omer ZAK02:03:06

So each catch clause will undo stuff and then re-throw the exception?

Bob B02:03:50

that seems like it would (be one way to) do it

Bob B02:03:04

A probably more unorthodox way would be to pass an undo stack along to the functions, or make it a dynamic binding and re-bind it as the calls go down. Then, if something goes bad, run the undo stack

Omer ZAK02:03:35

Your original suggestion effectively turns the catch clauses into an implicit undo stack.

nate05:03:21

@U5FV4MJHG and I have been exploring a similar problem (where there are several I/O touchpoints and logic needs to be used to retry or skip steps) in our latest series of the Functional Design in Clojure podcast. The pattern we came to at the end was a main loop that did three steps (each implemented as a function): 1. Think 2. Do 3. Assimilate All of the state of the entire process is contained in one map of data. The Think function takes that data and decides what the next operation should be. Then the Do function does that operation. Finally, the Assimilate function takes the result of the Do function and integrates that information into the big map of data. Then that map is passed into the Think function again, and so on. Because all of the decision logic about what the next operation should be is in one function, it can decide that the entire process should be aborted and issue operations to roll everything back. And since all previous results have been incorporated in the big data map, it has all the information it needs to know what to roll back. As a nice side effect, the Think and Assimilate functions are both pure, and therefore can be easily tested as well. I hope this all makes sense. The series I'm speaking of started at https://clojuredesign.club/episode/101-sportify/, and just wrapped up https://clojuredesign.club/episode/113-highlightify/. We've also been having great conversations about this in our podcast's channel: #clojuredesign-podcast

🙌 1