Clojureによるサービス/Bot開発まとめ

Service/Bot/Scheduler development with Clojure.

ClojureのBot開発についての知見をまとめていく. フロントエンドを持たないバックエンド開発, バックグラウンドタスク, スケジューラ, Botなどなど…

Clojureで手続きプログラミングなloopを書いたり状態を管理するベストプラクティスを知りたい. どうも手続き的な方法をそのまま導入することには違和感がある. もしくはフレームワークつかうか. 現在の仮の答えはステートチャート系のフレームワークの利用(clj-statechats).

HTTPプロトコルに依存するものは📝Clojure Web Development, もしくは📝Clojure API Server Development.

クローラー開発の定期実行はこのメモに, スクレイピング技術は📝Clojure Scrapingへ.

ドメイン特化はサブメモへ.

Clojureイベントループ実装

cronをclojureで実現しようとしたときは無限の遅延シーケンスを扱うことになる.

ネットの情報だとwebサーバが多いのだが, もはや📝Web Server Abstructionやそのフレームワークは内部の仕組みが隠蔽されて完成されているので, なかなかサーバのなかがなにをやっているのかよくわからない.

これはライブラリを使うのがいい. chimeがまず選択肢として上がる.

Clojureでの無限ループ

もっとも基本的な実装はloop/recurをつかった無限ループ実装. 簡単な処理ならこれでいい.

(loop []
    (Thread/sleep 1000)
    (recur))

core.async/timeoutをつかう

他には, 📝Clojure core.asyncのgo-loopをつかう.

ref. How to schedule a list of functions `n` seconds apart with Clojure Core Async - Stack Overflow

(a/go-loop []
  (a/<! (a/timeout 3000))
  (println "Hello!")
  (recur))

止まらないので注意. 止めるにはexit-chみたいな外部から操作できるchannelを別途用意.

componentやintegrantのようなClojure: 状態管理とシステムを導入して終了時に確実にchannelを閉じる仕組みをいれるのもよい.

(defmethod ig/init-key ::watcher [_ _]
  (let [ch (a/chan (a/sliding-buffer 1))]
    (a/go-loop []
      (if-let [msg (a/<! ch)]
        (do
          (do-something!)
          (a/<! (a/timeout 3000))
          (recur))
        (println "Done.")))
    ch))
 
(defmethod ig/halt-key! ::watcher [_ ch]
  (a/close! ch))

Clojure Schedular: 定期実行

📝cron的なものをClojureでどうやるか.

状態管理

うっかりREPLとかで無限ループを走らせるとREPLを再起動しないと止められないため, 状態管理の工夫が必要(start/stop). Clojureには状態管理ライブラリがあるのでそれらとくみあわせるといい.

🔧Clojure Integrant

以下の例ではduct(integrant)にchimeを組み込んでいる.

duct-frameworkに定時起動ジョブを仕込む - Hash λ Bye

状態遷移

Finate State Machine的なものを導入する.

Clojure FSM

FSMはReact界隈でも最近注目が集まっている(📝XState). この背景には宣言的なステートマシンの記述が関数型言語と相性がいいからかもしれない. するとClojureで状態を扱う方法としてのFSMは相性がいいことが類推できる.

Clojure: バックグラウンド実行

🔧Clojure: Chime

Clojureのスケジューラ開発のためのフレームワークで有名. チャイムと読む.

https://github.com/jarohen/chime

  • chime/chime-at: で指定時刻に実行.
  • chime/periodic-seq で無限ループ

chimeの設計

時間ごとの無限シーケンスをperiodic-seqで作成して, chime-atでその無限シーケンスを一つずつ消費していく. source(GitHub)

java.util.concurrent.ScheduleExecutorServiceの .schedule をつかっている.

ref. ScheduledExecutorService 使い方メモ - Qiita

💡chimeのループを止める方法

Returns an AutoCloseable that you can `.close` to stop the schedule. You can also deref the return value to wait for the schedule to finish.

(chime/chime-at)の戻り値をatomなどに保持しておいて, そのvalueに対して.closeを呼ぶと止まる.

(def state
  (atom ((chime/chime-at interval-seq
                         exec
                         {:on-finished stop})))
(.close @state)

💡エラー発生時の運用継続/中断

:error-handlerのタグにハンドラーを渡すとエラー時の処理を記述できる.

さらに関数がtrueを返すと運用継続, not trueならば運用中断する.

chimeで無限ループ(periodic-seq)

chime/periodic-seqをつかうと無限遅延シーケンスが生成できる. これをchime-atの引数に渡す.

chime-atで生成したオブジェクトは .closeをよばないとずっと動いているので注意.

(def sched (chime/chime-at (chime/periodic-seq (Instant/now) (Duration/ofSeconds 1))
                           (fn [time] (println "Chiming at" time))))
(.close sched)

chimeの無限ループをmain threadにくっつける方法

<!! をつかう.

(let [now (Instant/now)
      chimes (chime-ch [(.plusSeconds now 2)
                        (.plusSeconds now 3)])]
  (a/<!! (go-loop []
           (when-let [msg (<! chimes)]
             (prn "Chiming at:" msg)
             (recur)))))

READMEのサンプルそのものだが, a/<!!の部分でメインスレッドをブロッキングしている.

ref. How to make a Clojure go loop run forever - Stack Overflow

🔗References

Topics

💡Clojure ランタイム実行

ClojureはJVMランタイムが重いのでスクリプト用途には向かない

ClojureはJVMランタイムが重いのでスクリプト用途には向かない. たとえばclojure/clj -Xで実行するようなもの. 起動が遅く, メモリ使用量が大きい. 立ち上げるだけで数100MBが必要. Node.jsやRuby, Pythonのようなスクリプト要素では厳しい.

選択肢しては以下.


無限ループを止めるにはどうすればいいの?

Javaの仕組みを利用しているのでJavaの方法を検索するといろいろでてくる.

  • フラグで制御
    • (core.async/channelを仕込む)
  • 外部からのinterrupt
  • java.io.Closableを継承することによって.close呼び出し.

あまり自分で頑張らずになんかのフレームワークにのっかったほうがいいかも.

起動と終了(start/stop)の機能追加をするには?

Clojure: 状態管理ライブラリを利用する.