This week I've used (very basic) missionary to implement a stress test harness for our web server, reproducing certain "abuse" scenarios to validate hypothesis about the possible causes for problems we spotted in prod. I was impressed at how snappy cancellation was, and at being able to keep this REPL alive for three days as it killed my web server once and again. One of the conclusions of these experiments is that we need to improve at canceling long-running requests on this ring-jetty-backed server. Essentially, our problems boiled down to the kind of things that missionary takes care of best: cancellation and cleanup of worker processes.
In related matters, I was going to try and wrap https://stackoverflow.com/a/296435 in a missionary task, which then I could wrap with e/Task in Electric. Then I thought that someone must have done that already. Anyone?
I don't know how I convinced myself that this version works, but you need to remove all the locking until a better solution is found.
The locking tried to avoid a race condition preventing the query from being cancelled if it starts executing between the .cancel (which does nothing if the query has not started yet) and the .close (which does nothing if the query has started already). But .cancel cannot interrupt the execute! if they are mutually excluded.
I would rather check for thread interruption before running the statement, and use a separate task to cancel the statement. Example (untested) :
(defn execute-task
"`conn` is a next.jdbc Connection (NB: *not* a data source)
`sql` is the vector representing the query (e.g., [\"SELECT 1\"]).
`opts` are any other options you would supply to jdbc/execute!"
[conn sql & opts]
(m/sp (let [stmt (apply jdbc/prepare conn sql opts)]
(m/? (m/race
(m/sp (try (m/? m/never) (finally (.cancel stmt))))
(m/via m/blk
(try
(m/!)
(jdbc/execute! stmt)
;; XXX: how to tell a "cancelled" exception from others varies
;; across JDBC drivers, YMMV.
(catch MySQLStatementCancelledException _
(try
;; Only rollback if we're not in auto-commit mode
(when (not (.getAutoCommit conn))
(.rollback conn))
(catch SQLException _
;; possibly log or measure how often you get this?
))
(throw (ex-info "SQL query closed before executing" {})))
(finally
(.close stmt)))))))))Amazing! I finally had this working acceptably and I meant to update this thread, but I'll try that first. Adding (m/sp (try (m/? m/never) (finally (.cancel stmt)))) to my bag of tricks, thanks!
That works, except at least the m/via task needs to be wrapped in this m/attempt/`m/absolve` pattern around the m/race, like the implementation of m/timeout does, since that task can throw all kinds of exceptions that we want to propagate to the caller (e.g. SQL syntax errors, constraint violations, etc.). If left unwrapped, any exception would freeze the race forever
(or until cancelled from outside)
I'm using this pattern in a different use case where I want a timeout that is remotely started by signaling a dfv. I wonder whether a version of m/race that returns the first successful or failing result would be useful, both as a utility, and to call the user's attention on that nuance of how m/race itself works, which is easy to miss or forget.
(defn duel
"Like `race`, but ends as soon as any of `task` succeeds *or fails*, yielding the same
success or failure result."
[& tasks]
(->> tasks
(map attempt)
(apply race)
absolve))The name "duel" is because in old sword/muskeet duels the point was not so much to "win" (preferrable as that was to the alternative) as to back one's honor with one's life
oh you're right, I keep forgetting about this problem
I've also struggled to find a good name for this operator
Could you elaborate the duel metaphor ? I understand that a duel terminates when any principal withdraws, but how do task success/failure relate to duel outcomes ? Is a first wound considered success ?
I actually asked some LLM and it suggested sports/fighting metaphors like "sudden death" and "first blood".
I adopted the name duel for my own utility library on the basis that it is short and memorable, implies competition, and a race goes on if some participant withdraws early, but a duel is done as soon as the first contender is done one way or the other. It's far from a perfect metaphor or an obvious choice. But I think some of the shortcomings of the name are alleviated if the function is described in contrast with race.
DeepSeek suggested rush, which I like too, because it doesn't have the somewhat misleading connotation of two participants like duel does, and in contrast with race, rush is more about finishing quickly than about success. To me that word brings to mind higher risk of failure from focusing on speed.
Fixed a race condition: query would not be cancelled if we tried to do so before it had a chance to execute.
I forgot to throw the ex-infos.
When I define a m/ap flow using loop/recur, it is possible that I can notify that the flow has ended by simply not recurring anymore. I tried the same with m/?> by returning (reduced data) or by throwing missionary Cancelled, but both approaches do not work. Attached is a snippet that illustrates my issue. c-f should just print the first 4 items and then the reducer task should be finished normally.
very interesting @xifi So the ap flows are not consumed, unless requested downstream.
yes. It's supervision, eduction supervises ap and since it knows it's done it cleans up its children
(m/eduction (take 4) a-f)
Thanks @xifi this would work if I just want to take a Fixed number of items. But I would like to do this inside the ap where I use the forked operator, as I then can make it dependen on the state I have accumulated. It is also a good question because seed generated flows can end and flows generated with forks should also have the possibillity to end.
I see, thanks for the clarification. The pattern for that would be to return a sentinel value and check for it. E.g. return ::done and then (m/eduction (take-while (complement #{done}))`
yes, but then the fork of the ap process would still be running, and would still keep sending done in this case.
No it wouldn't, as eduction terminates it will terminate its child
Try it out in the REPL