I am having an issue with Process.monitor/1. My initial use case was to monitor Phoenix Channel and do some cleanup after it dies. However, I didn't manage to set it up in Phoenix and decided to test it out with pure GenServers.
So, I have a simple GenServer and I want to track when it dies:
defmodule Temp.Server do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, %{})
def init(args) do
Temp.Monitor.monitor(self())
{:ok, args}
end
end
And another GenServer that monitors:
defmodule Temp.Monitor do
use GenServer
require Logger
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def monitor(pid) do
Process.monitor(pid)
end
def handle_info({:DOWN, ref, :process, _, _}, state) do
Logger.info("DOWN")
{:noreply, state}
end
end
So, if I understand correctly, Process.monitor will start monitoring the Temp.Server process, and should call the handle_info matching :DOWN when Server process dies. If I try it in iex:
iex> {_, pid} = Temp.Server.start_link([])
{:ok, #PID<0.23068.3>}
iex> Process.exit(pid, :kill)
true
I expect handle_info being called from Monitor module and logging "DOWN", however that doesn't happen. What am I doing wrong? I assume it doesn't work because I call to monitor from Server process Temp.Monitor.monitor(self()), but I just can't figure out how else should I do it.
When you call the Temp.Monitor.monitor/1 method, it's still running in Temp.Server's own process, not Temp.Monitor's. That means the :DOWN message is sent to Temp.Server when Temp.Server dies, which is redundant.
What you want to do is, pass the pid of your server process to Temp.Monitor and have it call the Process.Monitor method from it's own process so it can monitor it. That can only happen from one of the GenServer callbacks.
You can do that by moving your implementation in to handle_call/3 or handle_cast/3:
defmodule Temp.Monitor do
use GenServer
require Logger
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def monitor(pid) do
GenServer.cast(__MODULE__, {:monitor, pid})
end
def handle_cast({:monitor, pid}, state) do
Process.monitor(pid)
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _, _}, state) do
Logger.info("DOWN")
{:noreply, state}
end
end
Related
I'm currently working on my first big elixir project and wanted to properly utilize testing this time.
However, if I add my Modules to the "normal" supervisor, i cannot start them again with start_supervised! and all tests fail with Reason: already started: #PID<0.144.0>
Here is my code:
(application.ex)
defmodule Websocks.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
#moduledoc false
use Application
def start(_type, _args) do
children = [
{Websocks.PoolSupervisor, []},
{Websocks.PoolHandler, %{}}
# {Websocks.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Websocks.Supervisor]
Supervisor.start_link(children, opts)
end
end
Some of my tests:
defmodule PoolHandlerTest do
use ExUnit.Case, async: true
alias Websocks.PoolHandler
doctest PoolHandler
setup do
start_supervised!({PoolHandler, %{}})
%{}
end
test "adding two pools and checking if they are there" do
assert PoolHandler.add(:first) == :ok
assert PoolHandler.add(:second) == :ok
assert PoolHandler.get_pools() == {:ok,%{:first => nil, :second => nil}}
end
and the pool handler:
defmodule Websocks.PoolHandler do
use GenServer
# Client
def start_link(default) when is_map(default) do
GenServer.start_link(__MODULE__, default, name: __MODULE__)
end
# Server (callbacks)
#impl true
def init(arg) do
{:ok, arg}
end
end
(I cut out the stuff i think is not necessary, but the complete code is on github here: github)
Thanks in advance for any help i get!
As #Everett mentioned in the comment - your application will already be started for you when you mix test, so there is no need to start your GenServers again. It seems like you're interacting with the global instance in your test, so if that's what you want, then it should just work.
However, if you'd like to start a separate instance just for your test, you need to start an unnamed one. For example, you could add an optional pid argument to your wrapper functions:
defmodule Websocks.PoolHandler do
# ...
def add(server \\ __MODULE__, value) do
GenServer.call(server, {:add, value})
end
# ...
end
Then, instead of using using start_supervised! like you do, you can start an unnamed instance in your setup and use it in your tests like so:
setup do
{:ok, pid} = GenServer.start_link(PoolHandler, %{})
{:ok, %{handler: pid}}
end
test "adding two pools and checking if they are there", %{handler: handler} do
PoolHandler.add(handler, :first)
# ...
end
Looking for a help with testing terminate/2 callback in my Channel.
Test and setup looks like this:
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
{:ok, socket} = connect(UserSocket, %{token: "some_token"})
{:ok, %{}, socket} = subscribe_and_join(socket, "some_channel", %{})
%{socket: socket}
end
test "terminate/2", %{socket: socket} do
# for avoiding "** (EXIT from #PID<...>) {:shutdown, :closed}"
Process.unlink(socket.channel_pid)
assert close(socket) == :ok
# some additional asserts go here
end
In terminate/2 method I just call a helper module, let's name it TerminationHandler.
def terminate(_reason, _socket) do
TerminationHandler.call()
end
And call/0 method in TerminationHandler contains a DB query. It can look like this i.e
def call() do
users = User |> where([u], u.type == "super") |> Repo.all # line where error appears
# some extra logic goes here
end
This is the error that I get periodically (maybe once in 10 runs)
14:31:29.312 [error] GenServer #PID<0.1041.0> terminating
** (stop) exited in: GenServer.call(#PID<0.1040.0>, {:checkout, #Reference<0.3713952378.42205187.247763>, true, 60000}, 5000)
** (EXIT) shutdown: "owner #PID<0.1039.0> exited with: shutdown"
(db_connection) lib/db_connection/ownership/proxy.ex:32: DBConnection.Ownership.Proxy.checkout/2
(db_connection) lib/db_connection.ex:928: DBConnection.checkout/2
(db_connection) lib/db_connection.ex:750: DBConnection.run/3
(db_connection) lib/db_connection.ex:644: DBConnection.execute/4
(ecto) lib/ecto/adapters/postgres/connection.ex:98: Ecto.Adapters.Postgres.Connection.execute/4
(ecto) lib/ecto/adapters/sql.ex:256: Ecto.Adapters.SQL.sql_call/6
(ecto) lib/ecto/adapters/sql.ex:436: Ecto.Adapters.SQL.execute_or_reset/7
(ecto) lib/ecto/repo/queryable.ex:133: Ecto.Repo.Queryable.execute/5
(ecto) lib/ecto/repo/queryable.ex:37: Ecto.Repo.Queryable.all/4
(my_app) lib/my_app/helpers/termination_handler.ex:4: MyApp.Helpers.TerminationHandler.call/0
(stdlib) gen_server.erl:673: :gen_server.try_terminate/3
(stdlib) gen_server.erl:858: :gen_server.terminate/10
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: {:join, Phoenix.Channel.Server}Last message: {:join, Phoenix.Channel.Server}
Would appreciate any responses regarding reasons of this error and possible ways to avoid it.
As stated in the documentation for GenServer.terminate/2:
[...] the supervisor will send the exit signal :shutdown and the GenServer will have the duration of the timeout to terminate. If after duration of this timeout the process is still alive, it will be killed immediately.
That is seemingly your case. DBConnection.checkout/2 seems to be waiting for the available connection to appear and this is lasted beyond the timeout. Hence the owner experiences a brutal kill.
There could be two possible solutions:
increase a timeout of shutdown (I would avoid that)
increase an amount of allowed simultaneous database connections.
The latter is likely needed in any case, since your pool seems to be full. That way the connection would be checked out immediately, and it should return in the timeout interval successfully.
This might help.
defmacro leave_channel(socket) do
quote do
Process.unlink(unquote(socket).channel_pid)
mref = Process.monitor(unquote(socket).channel_pid)
ref = leave(unquote(socket))
assert_reply ref, :ok
assert_receive {:DOWN, ^mref, :process, _pid, _reason}
end
end
defmacro close_socket(socket) do
quote do
Process.unlink(unquote(socket).channel_pid)
mref = Process.monitor(unquote(socket).channel_pid)
close(unquote(socket))
assert_receive {:DOWN, ^mref, :process, _pid, _reason}
end
end
I have been following along the RabbitMQ Work Queues tutorial for Elixir (Elixir Work Queues) which works quite nicely. On top of that I am now trying to get multiple consumers started and monitored by a Supervisor.
This last portion is proving to be a bit tricky. If I run the below code in 2 separate iex sessions, both are getting & handling messages from RabbitMQ.
Client (consumer)
defmodule MT.Client do
require Logger
#host Application.get_env(:mt, :host)
#username Application.get_env(:mt, :username)
#password Application.get_env(:mt, :password)
#channel Application.get_env(:mt, :channel)
def start_link do
MT.Client.connect
end
def connect do
{:ok, connection} = AMQP.Connection.open(host: #host, username: #username, password: #password)
{:ok, channel} = AMQP.Channel.open(connection)
AMQP.Queue.declare(channel, #channel, durable: true)
AMQP.Basic.qos(channel, prefetch_count: 1)
AMQP.Basic.consume(channel, #channel)
Logger.info "[*] Waiting for messages"
MT.Client.loop(channel)
end
def loop(channel) do
receive do
{:basic_deliver, payload, meta} ->
Logger.info "[x] Received #{payload}"
payload
|> to_char_list
|> Enum.count(fn x -> x == ?. end)
|> Kernel.*(1000)
|> :timer.sleep
Logger.info "[x] Done."
AMQP.Basic.ack(channel, meta.delivery_tag)
MT.Client.loop(channel)
end
end
end
Supervisor
defmodule MT.Client.Supervisor do
use Supervisor
require Logger
#name MTClientSupervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok, name: #name)
end
def init(:ok) do
children = [
worker(MT.Client, [], restart: :transient, id: "MTClient01"),
worker(MT.Client, [], restart: :transient, id: "MTClient02"),
worker(MT.Client, [], restart: :transient, id: "MTClient03")
]
supervise(children, strategy: :one_for_one)
end
end
When running that in an iex session:
iex -S mix
MT.Client.Supervisor.start_link
Following is logged:
08:46:50.746 [info] [*] Waiting for messages
08:46:50.746 [info] [x] Received {"job":"TestMessage","data":{"message":"message........"}}
08:46:58.747 [info] [x] Done.
08:46:58.748 [info] [x] Received {"job":"TestMessage","data":{"message":"last........"}}
08:47:06.749 [info] [x] Done.
So clearly there in only 1 consumer active, which is consuming the messages sequentially.
Running the below in 2 iex sessions:
MT.Client.start_link
I'm not adding the logs here, but in this case I get 2 consumes handling messages at the same time
I am sure that I am simply not grasping the required details for Agent/GenServer/Supervisor. Anyone can point out what needs to be changed to MT.Client & MT.Client.Supervisor above to achieve the idea of having multiple consumers active on the same channel?
Also; I'm been experimenting with spawning a Consumer agent and using the resulting pid in AMQP.Basic.consume(channel, #channel, pid) - but that's failing as well.
I'm integrating Bunny gem for RabbitMQ with Rails, should I start Bunny thread in an initializer that Rails starts with application start or do it in a separate rake task so I can start it in a separate process ?
I think if I'm producing messages only then I need to do it in Rails initializer so it can be used allover the app, but if I'm consuming I should do it in a separate rake task, is this correct ?
You are correct: you should not be consuming from the Rails application itself. The Rails application should be a producer, in which case, an initializer is the correct place to start the Bunny instance.
I essentially have this code in my Rails applications which publish messages to RabbitMQ:
# config/initializers/bunny.rb
MESSAGING_SERVICE = MessagingService.new(ENV.fetch("AMQP_URL"))
MESSAGING_SERVICE.start
# app/controllers/application_controller.rb
class ApplicationController
def messaging_service
MESSAGING_SERVICE
end
end
# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
def create
# save the model
messaging_service.publish_resize_image_request(model.id)
redirect_to uploads_path
end
end
# lib/messaging_service.rb
class MessagingService
def initialize(amqp_url)
#bunny = Bunny.new(amqp_url)
#bunny.start
at_exit { #bunny.stop }
end
attr_reader :bunny
def publish_resize_image_request(image_id)
resize_image_exchange.publish(image_id.to_s)
end
def resize_image_exchange
#resize_image_exchange ||=
channel.exchange("resize-image", passive: true)
end
def channel
#channel ||= bunny.channel
end
end
For consuming messages, I prefer to start executables without Rake involved. Rake will fork a new process, which will use more memory.
# bin/image-resizer-worker
require "bunny"
bunny = Bunny.new(ENV.fetch("AMQP_URL"))
bunny.start
at_exit { bunny.stop }
channel = bunny.channel
# Tell RabbitMQ to send this worker at most 2 messages at a time
# Else, RabbitMQ will send us as many messages as we can absorb,
# which would be 100% of the queue. If we have multiple worker
# instances, we want to load-balance between each of them.
channel.prefetch(2)
exchange = channel.exchange("resize-image", type: :direct, durable: true)
queue = channel.queue("resize-image", durable: true)
queue.bind(exchange)
queue.subscribe(manual_ack: true, block: true) do |delivery_info, properties, payload|
begin
upload = Upload.find(Integer(payload))
# somehow, resize the image and/or post-process the image
# Tell RabbitMQ we processed the message, in order to not see it again
channel.acknowledge(delivery_info.delivery_tag, false)
rescue ActiveRecord::RecordNotFound => _
STDERR.puts "Model does not exist: #{payload.inspect}"
# If the model is not in the database, we don't want to see this message again
channel.acknowledge(delivery_info.delivery_tag, false)
rescue Errno:ENOSPC => e
STDERR.puts "Ran out of disk space resizing #{payload.inspect}"
# Do NOT ack the message, in order to see it again at a later date
# This worker, or another one on another host, may have free space to
# process the image.
rescue RuntimeError => e
STDERR.puts "Failed to resize #{payload}: #{e.class} - #{e.message}"
# The fallback should probably be to ack the message.
channel.acknowledge(delivery_info.delivery_tag, false)
end
end
Given all that though, you may be better off with pre-built gems and using Rails' abstraction, ActiveJob.
Transactional fixtures in rspec prevent after_commit from being called, but even when I disable them with
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
The after_commit callback does not run.
Here is a rails app with the latest rspec / rails that I have produced the issue on:
git://github.com/sheabarton/after_commit_demo.git
One way around this is to trigger the commit callbacks manually. Example:
describe SomeModel do
subject { ... }
context 'after_commit' do
after { subject.run_callbacks(:commit) }
it 'does something' do
subject.should_receive(:some_message)
end
end
end
A little late, but hope this helps others.
In my case I resolved such problem with database_cleaner's settings placed below:
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :deletion
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
Thanks to Testing after_commit/after_transaction with Rspec
This is similar to #jamesdevar's answer above, but I couldn't add a code block, so I have to make a separate entry.
You don't have the change the strategy for the whole spec suite. You can keep using :transaction globally then just use :deletion or :truncation (they both work) as needed. Just add a flag to the relevant spec.
config.use_transactional_fixtures = false
config.before(:suite) do
# The :transaction strategy prevents :after_commit hooks from running
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each, :with_after_commit => true) do
DatabaseCleaner.strategy = :truncation
end
then, in your specs:
describe "some test requiring after_commit hooks", :with_after_commit => true do
If you're using database_cleaner you'll still run into this. I'm using the test_after_commit gem, and that seems to do the trick for me.
This Gist helped me.
It monkey-patches ActiveRecord to fire after_commit callbacks even if using transactional fixtures.
module ActiveRecord
module ConnectionAdapters
module DatabaseStatements
#
# Run the normal transaction method; when it's done, check to see if there
# is exactly one open transaction. If so, that's the transactional
# fixtures transaction; from the model's standpoint, the completed
# transaction is the real deal. Send commit callbacks to models.
#
# If the transaction block raises a Rollback, we need to know, so we don't
# call the commit hooks. Other exceptions don't need to be explicitly
# accounted for since they will raise uncaught through this method and
# prevent the code after the hook from running.
#
def transaction_with_transactional_fixtures(options = {}, &block)
rolled_back = false
transaction_without_transactional_fixtures do
begin
yield
rescue ActiveRecord::Rollback => e
rolled_back = true
raise e
end
end
if !rolled_back && open_transactions == 1
commit_transaction_records(false)
end
end
alias_method_chain :transaction, :transactional_fixtures
#
# The #_current_transaction_records is an stack of arrays, each one
# containing the records associated with the corresponding transaction
# in the transaction stack. This is used by the
# `rollback_transaction_records` method (to only send a rollback hook to
# models attached to the transaction being rolled back) but is usually
# ignored by the `commit_transaction_records` method. Here we
# monkey-patch it to temporarily replace the array with only the records
# for the top-of-stack transaction, so the real
# `commit_transaction_records` method only sends callbacks to those.
#
def commit_transaction_records_with_transactional_fixtures(commit = true)
unless commit
real_current_transaction_records = #_current_transaction_records
#_current_transaction_records = #_current_transaction_records.pop
end
begin
commit_transaction_records_without_transactional_fixtures
rescue # works better with that :)
ensure
unless commit
#_current_transaction_records = real_current_transaction_records
end
end
end
alias_method_chain :commit_transaction_records, :transactional_fixtures
end
end
end
Put this a new file in your Rails.root/spec/support directory, e.g. spec/support/after_commit_with_transactional_fixtures.rb.
Rails 3 will automatically load it in the test environment.