Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/plausible/ingestion/write_buffer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ defmodule Plausible.Ingestion.WriteBuffer do
{:noreply, %{state | buffer: [], buffer_size: 0, timer: timer}}
end

def handle_info({:EXIT, _from, _reason}, state) do
# We trap exits so terminate/2 can flush on shutdown. Linked ports and

@aerosol aerosol Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is unnecessary, let's log the reason instead (warning level is fine).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bonus points for asserting the log entry in test 🤗

# processes (e.g. the ClickHouse connection socket) deliver :EXIT messages
# here on disconnect, which we ignore to keep the buffer alive.
{:noreply, state}
end

@impl true
def handle_call(:flush, _from, state) do
%{timer: timer, flush_interval_ms: flush_interval_ms} = state
Expand Down
36 changes: 36 additions & 0 deletions test/plausible/ingestion/write_buffer_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Plausible.Ingestion.WriteBufferTest do
use ExUnit.Case, async: true

alias Plausible.Ingestion.WriteBuffer

@opts WriteBuffer.compile_time_prepare(Plausible.ClickhouseEventV2)

test "keeps running when a linked process exits while trapping exits" do
opts =
@opts
|> Map.take([:header, :insert_sql, :insert_opts])
|> Map.to_list()
|> Keyword.put(:name, :"write_buffer_exit_#{System.unique_integer([:positive])}")
# large interval so the empty buffer never flushes to ClickHouse during the test
|> Keyword.put(:flush_interval_ms, 60_000)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making this a bit more readable? Since compile_time_prepare result is a map, let's build a keyword list verbatim and only fetch necessary fields via the dot-notation access.

Something like:

test "...", %{test: test} do
opts = [
  header: @compile_time_opts.header,
  insert_sql: @compile_time_opts.insert_sql,
  insert_opts: @compile_time_opts.insert_opts,
  name: test, # unique atom is at our disposal already
  flush_interval_ms: 1_000_000_000 # no need to ever flush automatically on slow CI
]


{:ok, pid} = WriteBuffer.start_link(opts)

# The buffer traps exits, so a linked process exiting delivers a real
# {:EXIT, from, reason} message to handle_info/2. Linking a throwaway
# process to the buffer and killing it reproduces the crash in #6382
# without sending a synthetic message.
child =
spawn(fn ->
Process.link(pid)
Process.sleep(:infinity)
end)

ref = Process.monitor(child)
Process.exit(child, :shutdown)
assert_receive {:DOWN, ^ref, :process, ^child, _}, 1_000

assert Process.alive?(pid)
assert WriteBuffer.flush(opts[:name]) == :ok
end
end
Loading