trioでサーバーを立ち上げながらバックグラウンド処理を並列で実行(Quart-Trio)
モチベーション
IVRyのエンジニアの小瀬です。IVRy ではRailsを中心にバックエンドのコードを書いています。
タイトルだけみてやりたいことが伝え切れてない可能性が高いので、やりたいことと、趣味でやっているプロジェクトについて説明させていただきます。
IVRyでは主にRailsのコードを書いているのですが、趣味でPythonを使った音楽の自動演奏プログラムを作っています。音楽の自動演奏といってもやっていることはmidiを使った演奏指示であり、音はDAW(Abelton Liveなど)にある沢山の楽器の力を借りています!
簡単にいうと、この自動演奏に外部から指示を送りたいな、、と思ったのがモチベーションです。
自動演奏プログラムについて
先日セルフのマイルストンに置いていた24h自動演奏配信に成功しました
↓これはその時の様子です。(24hの動画は残ってませんでした。Youtubeに詳しくならないとw)
YouTubeのvideoIDが不正です
上記の演奏プログラムは、trio というpythonの並列処理ライブラリを用いて実現しています。
並列処理が必要な理由は、midiの再生とmidiを生成するロジックを常に並列で動かすためです。
また、前職のLOVOTの開発でtrioを使っていたため、trioをもっと使いたいという気持ちもありこの技術を選んでいます!
trioについて簡単に紹介
trioはPythonの並列処理ライブラリで、非同期処理を簡単に記述することができます。
LOVOTの開発時にこのtrioには大変お世話になり、GROOVE X社からtrioに使える便利なライブラリを二つ公開するほど、LOVOTでは使い倒しています。
サンプルコードはこの記事が分かりやすいです。
trioを使うと並列処理や、並列処理のうちいずれか1つが終了したら処理を中断するみたいなことを簡単に書くことができます。
ちなみに、GROOVE X社が開発したtrio-utilを使うともっと簡単に書けます。
例えば、二つの処理を並列で動かしていずれか一つの処理終了を待つには
await wait_any(partial(foo, 'hello'),
partial(bar, debug=True
と書くだけで実装することができます。trio-utilの宣伝でした。
バックグラウンドで演奏をし続けながら、外から指示を与えるにはどうしたらいい。。。?
上記技術を使って自動演奏を無限に続けることはできましたが、元々24h演奏と共に、これを使った作曲もしたいと思っていました。
そのために、この自動演奏は使い勝手が悪く、例えばいいベースのフレーズが生み出されている時に、「そのベース続けて!」「コード進行だけ変えてみて!」「ドラムだけ変えて!」みたいなリクエストを送りたいなという気持ちになりました。
これが今回実現したいことのモチベーションです。同じ状況な人はいないかもしれませんが笑、もし並列処理に加えてサーバーを起動したい人がいたら参考になれば嬉しいです!
Quart-Trioを使ってみる
実装方法は色々あると思いますが、このtrioの処理を変えることなくserverとしてAPIリクエストを待ち続け、受け取ったリクエストに応じて処理を変えるにはどうすればいいのかな、、と悩んだ末に
Quart-Trio というライブラリに出会いました。
このサイトにtrioの優れたライブラリ一覧が記載してあり、その中で見つけました。(この中に前職のGROOVE X社が作ったpuraというライブラリもありますがそちらも素晴らしいです!)
上記のサイトでライブラリについてこのように紹介されています。
Like Flask, but for Trio. A simple and powerful framework for building async web applications and REST APIs. Tip: this is an ASGI-based framework, so you’ll also need an HTTP server with ASGI support.
Flaskのように書けるのでかなりわかりやすいですね!
このライブラリを使うと、
from quart_trio import QuartTrio
app = QuartTrio(__name__)
@app.route('/')
async def hello():
return 'hello'
app.run()
上記のようにサーバーを立ち上げることができます。
trioのバックグランド処理と同時にサーバーを起動
さて本題の並列処理ですが、こちら下記のような実装で実現することができます
from quart_trio import QuartTrio
import trio
app = QuartTrio(__name__)
@app.before_serving
async def startup():
app.nursery.start_soon(background_task)
@app.route('/')
async def index():
await trio.sleep(0.01)
return 'hello'
async def background_task():
while True:
print('background_task working!!!!!')
await trio.sleep(2)
app.run()
まず
@app.before_serving
を使うことで、サーバー立ち上げ前に処理を実行することができます。
その中で、QuartTrioのnursery
を使うことで、background_taskを実行することができます。
試しに動かしてみると。
Running on http://127.0.0.1:5000 (CTRL + C to quit)
background_task working!!!!!
[2023-01-29 11:37:34 +0900] [46597] [INFO] Running on http://127.0.0.1:5000 (CTRL + C to quit)
background_task working!!!!!
background_task working!!!!!
...
サーバーが立ち上がる前にバックグラウンド処理が実行され、サーバーが立ち上がった後もバックグランド処理が実行されていることが確認できました!
よし!!これでまた趣味の開発が捗る!
API処理の中で重い処理をバックグラウンド実行できます
ユースケースとして多そうなのは、例えば、APIリクエストを受けた時に100秒かかるheavy_task
をトリガーだけして、APIとしては実行完了を待たずにreturnしたい、といったケース
このケースも簡単に書くことができます。
import datetime
from quart_trio import QuartTrio
import trio
app = QuartTrio(__name__)
@app.route('/')
async def index():
await trio.sleep(0.01)
app.nursery.start_soon(heavy_task)
print(f"API returned at {datetime.datetime.now()}")
return 'hello'
async def heavy_task():
while True:
await trio.sleep(100)
print(f"heavy task finished at {datetime.datetime.now()}")
app.run()
実行すると
Running on http://127.0.0.1:5000 (CTRL + C to quit)
[2023-01-29 11:49:51 +0900] [46961] [INFO] Running on http://127.0.0.1:5000 (CTRL + C to quit)
API returned at 2023-01-29 11:49:56.524730
[2023-01-29 11:49:56 +0900] [46961] [INFO] 127.0.0.1:53510 GET / 1.1 200 5 17140
heavy task finished at 2023-01-29 11:51:36.528263
期待通りAPIは25msで返答し、約100sかけてheavy_taskを実行することができました。
簡単にこういった並列処理が書けるのは便利ですね!
最後に会社の宣伝
株式会社IVRyはエンジニアをめちゃくちゃ募集しています。
IVRyの特徴として、本質思考で、面白い人が多く、一緒に働いていてとても楽しい環境です。
そして、事業もめちゃくちゃ伸びています!ベンチャーでチャレンジしたいエンジニアさん、IVRyはかなり良い選択だなと思いますので、ぜひご検討を!
気になった方はカジュアル面談も可能ですので、ご連絡ください!
Discussion