この記事は何
Rubyには3.0からRactorという機能がexperimentalで入っています。
これは文字通りRubyでActorモデルを実装することを可能にしたものです。
この記事では、このRactorを用いて、ElixirやErlangのOTPの一つであるGenServerを作ってみた記録記事です。
Ractorとは
Ractorとは、Ruby 3.0から導入された新しい並行抽象化の機能です。
今までのThreadなどの仕組みとは異なり、GVLの制限を受けずに文字通り並列の処理をRubyで実現することが可能になります。
詳しくは以下の記事をご覧ください。
OTPとは
OTP (Open Telegraph Protocol)とは、分散システムや並列処理をErlangやElixirで実現するためのフレームワークです。このOTPではいわゆるアクターモデルを用いた実装がされています。
OTPの一つとして、GenServerがあります。
GenServerとは、関数型言語であるErlangやElixirで状態を保持するために用いるパターンです。
GenServerでは、状態を変数ではなく、プロセスベースで保持します。
詳しくはこちらをご覧ください。
実装例
今回はこのErlangやElixirで用いられるGenServerを、同じくアクターモデルを実現するRactorを用いて実装していきたいと思います。
今回は以下のように実装を行なってみました。
class RactorGenServer
def call(command, data = nil)
ractor.send([command, data])
ractor.take
end
def cast(command, data = nil)
ractor.send([command, data])
end
private
def ractor
@ractor ||= Ractor.new do
handle_message = ->(message, current_state) do
case message
in [:enqueue, data]
{
status: :ok,
state: [*current_state, data],
value: nil
}
in [:dequeue, nil]
if current_state.empty?
{ status: :error,
state: current_state,
value: nil
}
else
{
status: :ok,
state: current_state[1..-1],
value: current_state.first
}
end
end
end
state = []
loop do
message = Ractor.receive
handle_message.call(message, state) => { status:, state:, value: }
raise StandardError, "error" if status == :error
Ractor.yield(value) if value
end
end
end
end
# 使用例
gen_server = RactorGenServer.new
gen_server.cast(:enqueue, "item1")
gen_server.cast(:enqueue, "item2")
puts gen_server.call(:dequeue) # => "item1"
puts gen_server.call(:dequeue) # => "item2"
このような実装をすることで、Ractorのプロセス単位で状態を保持することが可能になります。
OTPでは、分散システムで扱われる想定のための実装も含まれていますが、今回はそこまで実装はしておらず、あくまでプロセス単位で状態を保持できるところまで実装してみました。
今回はRactorの使い方の模索の一環として、OTPのGenServerっぽいものを作ってみました。
まだだいぶ荒削りな実装なので、
よりOTPの挙動に近い実装のアイディアなどある方はぜひシェアいただけると嬉しいです。