Is there a canonical way to ensure that only one instance of a service is running / starting / stopping in Clojure?

I am writing a status server in Clojure supported by Neo4j that can handle socket requests like HTTP. This means, of course, that I need to be able to start and stop socket servers from this server. Of course, I would like to be able to declare a “service” on this server and start and stop it.

What I'm trying to include in Clojure is how to make these services start and stop thread-safe. This server, which I am writing, will have NREPL built inside it and process incoming requests in parallel. Some of these requests will be administrative: start service X, stop service Y. This opens up the possibility of simultaneously launching two start requests.

  • The start should synchronously check the "running" flag and the "start" flag and fail, if set. In the same transaction, the "start" flag must be set.
  • After the "start" flag is set, the transaction is completed. This makes the start flag visible to other transactions.
  • Then the (start) function actually starts the service.
  • If (start) succeeds, the “start” and “start” flags are set synchronously.
  • If (start) fails, the "start" flag is set and an exception is returned.

Stopping requires the same by checking the "running" flag and checking and setting its own "stopping" flag.

I try to reason through all possible combinations of (start) and (stop).

Did I miss something?

Is there a library for this? If not, what should the library look like? I will open the source and put it on Github.

Edit:

This is what I still have. There is a hole that I see. What am I missing?

(ns extenium.db (:require [clojure.tools.logging :as log]) (:import org.neo4j.graphdb.factory.GraphDatabaseFactory)) (def ^:private db- (ref {:ref nil :running false :starting false :stopping false})) (defn stop [] (dosync (if (or (not (:running (ensure db-))) (:stopping (ensure db-))) (throw (IllegalStateException. "Database already stopped or stopping.")) (alter db- assoc :stopping true))) (try (log/info "Stopping database") (.shutdown (:ref db-)) (dosync (alter db- assoc :ref nil)) (log/info "Stopped database") (finally (dosync (alter db- assoc :stopping false))))) 

In the try block, I register, then call .shutdown, and then register again. If the first log fails (there may be I / O exceptions), then (: stopdb-) is set to false, which unlocks it and excellent .. shutdown is the void function from Neo4j, so I do not need to evaluate the return value. If it fails, (: stopdb-) is set to false, which is also very good. Then I set (: ref db-) to nil. What if it fails? (: stopdb-) is set to false, but (: ref db-) remains hanging. So there is a hole. The same case with the second log call. What am I missing?

Would it be better if I just used Clojure blocking primitives instead of dancing ref?

+3
source share
2 answers

This is the natural fit for a simple lock:

 (locking x (do-stuff)) 

Here x is the object to synchronize to.

To develop: starting and stopping a service is a side effect; side effects should not be triggered within a transaction, with the possible exception of agent actions. Here, although locks are exactly what the design requires. Please note: there is nothing wrong with using them in Clojure when they are well suited to the problem in question, in fact I would say that locking is the canonical solution here. (See Stuart Halloway Lancet , presented in the Clojure Program (1st ed.), For an example of a Clojure library that uses locks that has been widely used, mainly in Leningen.)

Update: Adding Failover Behavior:

This is still suitable for blocking, namely java.util.concurrent.locks.ReentrantLock (see link for Javadoc):

 (import java.util.concurrent.locks.ReentrantLock) (def lock (ReentrantLock.)) (defn start [] (if (.tryLock lock) (try (do-stuff) (finally (.unlock lock))) (do-other-stuff))) 

(do-stuff) will be executed if the lock capture is complete; otherwise (do-other-stuff) will happen. The current thread will not be blocked in both cases.

+3
source

This sounds like a good precedent for agents; they allow you to serialize changes to a piece of mutable state; Clojure Agent Documentation has a good overview. You can use error handlers and agent methods to handle exceptions and don't have to worry about locks or race conditions.

 (def service (agent {:status :stopped})) (defn start-service [{:keys [status] :as curr}] (if (= :stopped status) (do (println "starting service") {:status :started}) (do (println "service already running") curr))) ;; start the service like this (send-off service start-service) ;; gets the current status of the service @service 
+1
source

Source: https://habr.com/ru/post/1484706/


All Articles