Elixir的进程管理
因为身处的行业以保守而闻名,工作中经常使用的编程语言也是因为落后与守旧而臭名昭著的Java
语言。 当初一直认为,相比很多现代的编程语言,Java虽然缺少很多语法糖,但是强大的生态足够让它在生产侧笑傲江湖。 前些日子,因为生产上遇到了问题, 尝试了各种方法都没有达到自己想要的效果, 但是这个问题或许在其他编程语言中根本不会存在。 这才觉察到学一门新的语言,相当于换一种活法,也许比想象中的要愉快很多。
我们在这个问题暴露出来的需求是什么呢? 是想要中断正在执行的任务,并且,这种中断的手段应该在具体的定时任务实现以外。
现在用Elixir一个简单的定时任务系统,先复现生产上的问题,然后再看看用Elixir如何解决它。
创建一个应用
创建一个有监督树的应用。
mix new hello_world --sup
defmodule HelloWorld.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: HelloWorld.Worker.start_link(arg)
# {HelloWorld.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
Supervisor.start_link(children, opts)
end
end
children里是将要被Supervisor启动的进程,而opts是进程树的配置参数 其中strategy的:one_for_one,的意思是,当一个进程崩溃后,只重启他自己。
为了还原现场,我们先实现一个死循环的方法,作为任务。
defmodule HelloWorld.Job do
def infinite_hello(words) do
Process.sleep(1000)
IO.puts("Hello, #{words}!")
infinite_hello(words)
end
end
定时任务需要调度器。像是这种需要后台一直执行,存在状态的需求,用GenServer非常好实现。
在application.ex中,注册这个调度器,然后启动应用。
可以看到,控制台在不停的输出"Hello, World!"
我们希望能够终止掉这个行为,所以我们决定从最简单的开始,停止这个调度器。
defmodule HelloWorld.Scheduler do
use GenServer
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init(_args) do
:timer.apply_after(1000, __MODULE__, :start_job, [])
:timer.apply_after(10000, __MODULE__, :terminate_job, [])
{:ok, %{}}
end
def terminate_job do
GenServer.stop(__MODULE__, :normal)
end
@spec start_job() :: any()
def start_job do
GenServer.call(__MODULE__, :start_job)
end
def handle_call(:start_job, _from, state) do
case state do
%{pid: pid, ref: _ref} ->
IO.puts("Job already running with PID: #{inspect(pid)}")
{:reply, "Job already running", state}
%{} ->
{pid, ref} =
spawn_monitor(fn ->
HelloWorld.Job.infinite_hello("World")
end)
{:reply, "Job started", %{pid: pid, ref: ref}}
end
end
def handle_info({:DOWN, ref, :process, pid, reason}, state) do
if state.ref == ref do
IO.puts("Process #{inspect(pid)} terminated with reason: #{inspect(reason)}")
{:noreply, reason, state}
else
{:noreply, state}
end
end
# 让调度器能能够停止
def terminate(reason, state) do
IO.puts("Scheduler terminating with reason: #{inspect(reason)}")
case state do
%{pid: pid, ref: _ref} ->
Process.exit(pid, :normal)
IO.puts("Job with PID #{inspect(pid)} terminated")
_ ->
:ok
end
end
end
我们用spawn_monitor启动一个子进程来运行我们的定时任务,在10s后停止这个调度器的同时,给子进程发送终止信号。
不过我们运行起来这个项目会发现,控制台的日志输出在10s内非但没有停止,而且每过10s都会新增一个线程不停的输出。 这是因为Supervisor在我们终止GenServer以后,它会有一次把它拉起来。 在init/1方法里又启动了一个子进程。
并且最重要的是, Process.exit(:normal)并不能让子进程结束,所以我们需要修改两个地方,让那个整个应用能够正常结束。
- Process.eixt(:normal)改成:kill
- 通过GenServer的chil_spec把自己设定成不会重启的任务
def child_spec(_args) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [[]]},
restart: :temporary,
type: :worker
}
end