如何实现Elixir GenServer每秒发起请求且无需等待前请求完成
如何让GenServer准时每秒发起HTTP请求(避免同步请求阻塞调度)
你的问题核心在于同步HTTP请求阻塞了GenServer的消息循环——原来的handle_info(:jpeg_fetch, state)里,make_jpeg_request是同步执行的,必须等请求完成(2-3秒)才能调用schedule_fetch_call,导致下一次请求的触发时间被延后了请求耗时的长度。
要实现「不管前一次请求是否完成,每秒准时发起新请求」的目标,不需要手动spawn进程,利用Finch本身支持的异步请求就能解决,同时保持GenServer的消息循环不被阻塞。下面是具体的改造方案:
改造步骤
1. 把同步HTTP请求改成异步
Finch支持异步请求模式,我们可以让请求在后台由Finch的池进程处理,同时把请求上下文(比如相机信息、请求时间)传递给响应回调,这样请求完成后能把结果发回给GenServer。
修改make_jpeg_request函数(如果你的Everjamer封装了Finch,确保它能传递async选项;如果没有,直接用Finch原生API):
defp make_jpeg_request(camera) do headers = get_request_headers(camera.auth, camera.username, camera.password) requested_at = DateTime.utc_now() # 发起异步请求,把相机信息和请求时间作为上下文传递 Finch.request( :get, camera.url, headers, async: {self(), {camera, requested_at}}, pool: :your_finch_pool_name # 替换成你实际的Finch池名称 ) end
2. 调整handle_info的职责
原来的:jpeg_fetch消息现在只负责准时调度下一次请求和发起新的异步请求,不再等待请求完成:
def handle_info(:jpeg_fetch, state) do # 先调度下一次请求,确保1秒后准时触发,不受当前请求耗时影响 schedule_fetch_call(state.sleep) # 发起异步请求,后台执行,不阻塞当前GenServer进程 make_jpeg_request(state.camera) {:noreply, state} end
3. 添加新的handle_info处理异步响应
Finch会在异步请求完成后,向GenServer发送{:finch_response, ...}或{:finch_error, ...}消息,我们需要添加对应的处理逻辑,完成结果存储和状态更新:
# 处理成功的HTTP响应 def handle_info({:finch_response, _ref, {:ok, %Finch.Response{body: body, status: 200, headers: headers}}, {camera, requested_at}}, state) do # 构建存储用的数据(从响应头提取真实文件大小,替换原来的硬编码值) for_jpeg_bank = %{ datetime: requested_at, image: body, file_size: get_content_length(headers) } put_it_in_jpeg_bank(for_jpeg_bank, camera.name) # 更新running状态 new_state = Map.put(state, :running, %{datetime: requested_at}) {:noreply, new_state} end # 处理HTTP请求失败(比如状态码非200) def handle_info({:finch_response, _ref, _error, {camera, requested_at}}, state) do put_it_in_jpeg_bank(%{}, camera.name) new_state = Map.put(state, :running, %{datetime: requested_at}) {:noreply, new_state} end # 处理Finch连接错误(比如网络问题) def handle_info({:finch_error, _ref, _reason, {camera, requested_at}}, state) do put_it_in_jpeg_bank(%{}, camera.name) new_state = Map.put(state, :running, %{datetime: requested_at}) {:noreply, new_state} end # 从响应头提取Content-Length defp get_content_length(headers) do headers |> Enum.find_value(fn {"content-length", len} -> len end) |> Kernel.||("0") end
为什么这样能解决问题?
- GenServer的消息循环不再阻塞:
:jpeg_fetch的处理逻辑变得非常轻量——只调度下一次请求和发起异步请求,几毫秒内就能完成,保证每秒准时触发新的请求。 - HTTP请求在后台执行:Finch的池进程会处理实际的HTTP请求,完成后再把结果发回GenServer处理,不会影响GenServer的调度节奏。
- 无需手动管理进程:利用Finch的异步机制,不需要自己
spawn或Task.start,代码更简洁且符合OTP规范。
注意事项
- 确保你的Finch池配置了足够的进程数(在
config.exs里设置size和max_overflow),避免大量并发请求导致排队。 - 如果
Everjamer是你自己封装的HTTP客户端,要确保它支持传递async选项给Finch,否则直接使用Finch原生API更稳妥。
内容的提问来源于stack exchange,提问作者Junaid Farooq
相关产品推荐
相关产品推荐

