Skip to content

Commit 0df6f09

Browse files
committed
feat: v2 serializer
1 parent 7c0b073 commit 0df6f09

File tree

11 files changed

+1395
-42
lines changed

11 files changed

+1395
-42
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Integration Tests
2+
on:
3+
pull_request:
4+
paths:
5+
- "lib/**"
6+
- "test/**"
7+
- "config/**"
8+
- "priv/**"
9+
- "assets/**"
10+
- "rel/**"
11+
- "mix.exs"
12+
- "Dockerfile"
13+
- "run.sh"
14+
- "docker-compose.test.yml"
15+
16+
push:
17+
branches:
18+
- main
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
tests:
26+
name: Tests
27+
runs-on: blacksmith-8vcpu-ubuntu-2404
28+
29+
steps:
30+
- uses: actions/checkout@v2
31+
- name: Run integration test
32+
run: docker compose -f docker-compose.tests.yml up --abort-on-container-exit --exit-code-from test-runner
33+

docker-compose.tests.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
services:
2+
# Supabase Realtime service
3+
test_db:
4+
image: supabase/postgres:14.1.0.105
5+
container_name: test-realtime-db
6+
ports:
7+
- "5532:5432"
8+
volumes:
9+
- ./dev/postgres:/docker-entrypoint-initdb.d/
10+
command: postgres -c config_file=/etc/postgresql/postgresql.conf
11+
environment:
12+
POSTGRES_HOST: /var/run/postgresql
13+
POSTGRES_PASSWORD: postgres
14+
healthcheck:
15+
test: ["CMD-SHELL", "pg_isready -U postgres"]
16+
interval: 10s
17+
timeout: 5s
18+
retries: 5
19+
test_realtime:
20+
depends_on:
21+
- test_db
22+
build: .
23+
container_name: test-realtime-server
24+
ports:
25+
- "4000:4000"
26+
extra_hosts:
27+
- "host.docker.internal:host-gateway"
28+
environment:
29+
PORT: 4000
30+
DB_HOST: host.docker.internal
31+
DB_PORT: 5532
32+
DB_USER: postgres
33+
DB_PASSWORD: postgres
34+
DB_NAME: postgres
35+
DB_ENC_KEY: 1234567890123456
36+
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
37+
API_JWT_SECRET: super-secret-jwt-token-with-at-least-32-characters-long
38+
SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
39+
ERL_AFLAGS: -proto_dist inet_tcp
40+
RLIMIT_NOFILE: 1000000
41+
DNS_NODES: "''"
42+
APP_NAME: realtime
43+
RUN_JANITOR: true
44+
JANITOR_INTERVAL: 60000
45+
LOG_LEVEL: "info"
46+
SEED_SELF_HOST: true
47+
networks:
48+
test-network:
49+
aliases:
50+
- realtime-dev.local
51+
- realtime-dev.localhost
52+
healthcheck:
53+
test: ["CMD", "curl", "-f", "http://localhost:4000/"]
54+
interval: 10s
55+
timeout: 5s
56+
retries: 5
57+
start_period: 5s
58+
59+
# Deno test runner
60+
test-runner:
61+
image: denoland/deno:alpine-2.5.6
62+
container_name: deno-test-runner
63+
depends_on:
64+
test_realtime:
65+
condition: service_healthy
66+
test_db:
67+
condition: service_healthy
68+
volumes:
69+
- ./test/integration/tests.ts:/app/tests.ts:ro
70+
working_dir: /app
71+
command: >
72+
sh -c "
73+
echo 'Running tests...' &&
74+
deno test tests.ts --allow-import --no-check --allow-read --allow-net --trace-leaks --allow-env=WS_NO_BUFFER_UTIL
75+
"
76+
networks:
77+
- test-network
78+
extra_hosts:
79+
- "realtime-dev.localhost:host-gateway"
80+
81+
networks:
82+
test-network:
83+
driver: bridge

lib/realtime_web/channels/realtime_channel/broadcast_handler.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ defmodule RealtimeWeb.RealtimeChannel.BroadcastHandler do
101101
end
102102

103103
defp send_message(tenant_id, self_broadcast, tenant_topic, payload) do
104-
broadcast = %Phoenix.Socket.Broadcast{topic: tenant_topic, event: @event_type, payload: payload}
104+
broadcast = build_broadcast(tenant_topic, payload)
105105

106106
if self_broadcast do
107107
TenantBroadcaster.pubsub_broadcast(
@@ -123,6 +123,20 @@ defmodule RealtimeWeb.RealtimeChannel.BroadcastHandler do
123123
end
124124
end
125125

126+
# Message payload was built by V2 Serializer which was originally UserBroadcast
127+
defp build_broadcast(topic, {user_event, user_payload_encoding, user_payload}) do
128+
%RealtimeWeb.Socket.UserBroadcast{
129+
topic: topic,
130+
user_event: user_event,
131+
user_payload_encoding: user_payload_encoding,
132+
user_payload: user_payload
133+
}
134+
end
135+
136+
defp build_broadcast(topic, payload) do
137+
%Phoenix.Socket.Broadcast{topic: topic, event: @event_type, payload: payload}
138+
end
139+
126140
defp increment_rate_counter(%{assigns: %{policies: %Policies{broadcast: %BroadcastPolicies{write: false}}}} = socket) do
127141
socket
128142
end

lib/realtime_web/channels/realtime_channel/message_dispatcher.ex

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,74 @@ defmodule RealtimeWeb.RealtimeChannel.MessageDispatcher do
44
"""
55

66
require Logger
7+
alias Phoenix.Socket.Broadcast
8+
alias RealtimeWeb.Socket.UserBroadcast
79

810
def fastlane_metadata(fastlane_pid, serializer, topic, log_level, tenant_id, replayed_message_ids \\ MapSet.new()) do
911
{:rc_fastlane, fastlane_pid, serializer, topic, log_level, tenant_id, replayed_message_ids}
1012
end
1113

14+
@presence_diff "presence_diff"
15+
1216
@doc """
1317
This dispatch function caches encoded messages if fastlane is used
1418
It also sends an :update_rate_counter to the subscriber and it can conditionally log
15-
"""
16-
@spec dispatch(list, pid, Phoenix.Socket.Broadcast.t()) :: :ok
17-
def dispatch(subscribers, from, %Phoenix.Socket.Broadcast{event: event} = msg) do
18-
# fastlane_pid is the actual socket transport pid
19-
# This reduce caches the serialization and bypasses the channel process going straight to the
20-
# transport process
21-
22-
message_id = message_id(msg.payload)
2319
20+
fastlane_pid is the actual socket transport pid
21+
"""
22+
@spec dispatch(list, pid, Broadcast.t() | UserBroadcast.t()) :: :ok
23+
def dispatch(subscribers, from, %Broadcast{event: @presence_diff} = msg) do
2424
{_cache, count} =
2525
Enum.reduce(subscribers, {%{}, 0}, fn
2626
{pid, _}, {cache, count} when pid == from ->
2727
{cache, count}
2828

29-
{pid, {:rc_fastlane, fastlane_pid, serializer, join_topic, log_level, tenant_id, replayed_message_ids}},
29+
{_pid, {:rc_fastlane, fastlane_pid, serializer, join_topic, log_level, tenant_id, _replayed_message_ids}},
3030
{cache, count} ->
31-
if already_replayed?(message_id, replayed_message_ids) do
32-
# skip already replayed message
33-
{cache, count}
34-
else
35-
if event != "presence_diff", do: send(pid, :update_rate_counter)
31+
maybe_log(log_level, join_topic, msg, tenant_id)
3632

37-
maybe_log(log_level, join_topic, msg, tenant_id)
38-
39-
cache = do_dispatch(msg, fastlane_pid, serializer, join_topic, cache)
40-
{cache, count + 1}
41-
end
33+
cache = do_dispatch(msg, fastlane_pid, serializer, join_topic, cache, tenant_id, log_level)
34+
{cache, count + 1}
4235

4336
{pid, _}, {cache, count} ->
4437
send(pid, msg)
4538
{cache, count}
4639
end)
4740

4841
tenant_id = tenant_id(subscribers)
49-
increment_presence_counter(tenant_id, event, count)
42+
increment_presence_counter(tenant_id, msg.event, count)
5043

5144
:ok
5245
end
5346

54-
defp increment_presence_counter(tenant_id, "presence_diff", count) when is_binary(tenant_id) do
55-
tenant_id
56-
|> Realtime.Tenants.presence_events_per_second_key()
57-
|> Realtime.GenCounter.add(count)
58-
end
47+
def dispatch(subscribers, from, msg) do
48+
message_id = message_id(msg)
5949

60-
defp increment_presence_counter(_tenant_id, _event, _count), do: :ok
50+
_ =
51+
Enum.reduce(subscribers, %{}, fn
52+
{pid, _}, cache when pid == from ->
53+
cache
54+
55+
{pid, {:rc_fastlane, fastlane_pid, serializer, join_topic, log_level, tenant_id, replayed_message_ids}},
56+
cache ->
57+
if already_replayed?(message_id, replayed_message_ids) do
58+
# skip already replayed message
59+
cache
60+
else
61+
send(pid, :update_rate_counter)
62+
63+
maybe_log(log_level, join_topic, msg, tenant_id)
64+
65+
do_dispatch(msg, fastlane_pid, serializer, join_topic, cache, tenant_id, log_level)
66+
end
67+
68+
{pid, _}, cache ->
69+
send(pid, msg)
70+
cache
71+
end)
72+
73+
:ok
74+
end
6175

6276
defp maybe_log(:info, join_topic, msg, tenant_id) do
6377
log = "Received message on #{join_topic} with payload: #{inspect(msg, pretty: true)}"
@@ -66,30 +80,57 @@ defmodule RealtimeWeb.RealtimeChannel.MessageDispatcher do
6680

6781
defp maybe_log(_level, _join_topic, _msg, _tenant_id), do: :ok
6882

69-
defp message_id(%{"meta" => %{"id" => id}}), do: id
70-
defp message_id(_), do: nil
71-
72-
defp already_replayed?(nil, _replayed_message_ids), do: false
73-
defp already_replayed?(message_id, replayed_message_ids), do: MapSet.member?(replayed_message_ids, message_id)
74-
75-
defp do_dispatch(msg, fastlane_pid, serializer, join_topic, cache) do
83+
defp do_dispatch(msg, fastlane_pid, serializer, join_topic, cache, tenant_id, log_level) do
7684
case cache do
77-
%{^serializer => encoded_msg} ->
85+
%{^serializer => {:ok, encoded_msg}} ->
7886
send(fastlane_pid, encoded_msg)
7987
cache
8088

89+
%{^serializer => {:error, reason}} ->
90+
# We do nothing at this stage. It has been already logged depending on the log level
91+
cache
92+
8193
%{} ->
8294
# Use the original topic that was joined without the external_id
8395
msg = %{msg | topic: join_topic}
84-
encoded_msg = serializer.fastlane!(msg)
85-
send(fastlane_pid, encoded_msg)
86-
Map.put(cache, serializer, encoded_msg)
96+
97+
result =
98+
case fastlane!(serializer, msg) do
99+
{:ok, encoded_msg} ->
100+
send(fastlane_pid, encoded_msg)
101+
{:ok, encoded_msg}
102+
103+
{:error, reason} ->
104+
maybe_log(log_level, join_topic, reason, tenant_id)
105+
end
106+
107+
Map.put(cache, serializer, result)
87108
end
88109
end
89110

90-
defp tenant_id([{_pid, {:rc_fastlane, _, _, _, _, tenant_id, _}} | _]) do
91-
tenant_id
111+
# We have to convert because V1 does not know how to process UserBroadcast
112+
defp fastlane!(Phoenix.Socket.V1.JSONSerializer = serializer, %UserBroadcast{} = msg) do
113+
with {:ok, msg} <- UserBroadcast.convert_to_json_broadcast(msg) do
114+
{:ok, serializer.fastlane!(msg)}
115+
end
92116
end
93117

118+
defp fastlane!(serializer, msg), do: {:ok, serializer.fastlane!(msg)}
119+
120+
defp tenant_id([{_pid, {:rc_fastlane, _, _, _, _, tenant_id, _}} | _]), do: tenant_id
94121
defp tenant_id(_), do: nil
122+
123+
defp increment_presence_counter(tenant_id, "presence_diff", count) when is_binary(tenant_id) do
124+
tenant_id
125+
|> Realtime.Tenants.presence_events_per_second_key()
126+
|> Realtime.GenCounter.add(count)
127+
end
128+
129+
defp increment_presence_counter(_tenant_id, _event, _count), do: :ok
130+
131+
defp message_id(%Broadcast{payload: %{"meta" => %{"id" => id}}}), do: id
132+
defp message_id(_), do: nil
133+
134+
defp already_replayed?(nil, _replayed_message_ids), do: false
135+
defp already_replayed?(message_id, replayed_message_ids), do: MapSet.member?(replayed_message_ids, message_id)
95136
end

lib/realtime_web/endpoint.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ defmodule RealtimeWeb.Endpoint do
2525
# the expense of potentially higher memory being used.
2626
active_n: 100,
2727
# Skip validating UTF8 for faster frame processing.
28-
# Currently all text frames as handled only with JSON which already requires UTF-8
28+
# Currently all text frames are handled only with JSON which already requires UTF-8
2929
validate_utf8: false,
3030
serializer: [
3131
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
32-
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
32+
{RealtimeWeb.Socket.V2Serializer, "~> 2.0.0"}
3333
]
3434
],
3535
longpoll: [

lib/realtime_web/plugs/auth_tenant.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ defmodule RealtimeWeb.AuthTenant do
4242
[] ->
4343
nil
4444

45+
[""] ->
46+
nil
47+
4548
[value | _] ->
4649
[bearer, token] = value |> String.split(" ")
4750
bearer = String.downcase(bearer)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule RealtimeWeb.Socket.UserBroadcast do
2+
@moduledoc """
3+
Defines a message sent from pubsub to channels and vice-versa.
4+
5+
The message format requires the following keys:
6+
7+
* `:topic` - The string topic or topic:subtopic pair namespace, for example "messages", "messages:123"
8+
* `:user_event`- The string user event name, for example "my-event"
9+
* `:user_payload_encoding`- :json or :binary
10+
* `:user_payload` - The actual message payload
11+
12+
Optionally metadata which is a map to be JSON encoded
13+
"""
14+
15+
alias Phoenix.Socket.Broadcast
16+
17+
@type t :: %__MODULE__{}
18+
defstruct topic: nil, user_event: nil, user_payload: nil, user_payload_encoding: nil, metadata: nil
19+
20+
@spec convert_to_json_broadcast(t) :: {:ok, Broadcast.t()} | {:error, String.t()}
21+
def convert_to_json_broadcast(%__MODULE__{user_payload_encoding: :json} = user_broadcast) do
22+
payload = %{
23+
"event" => user_broadcast.user_event,
24+
"payload" => Jason.Fragment.new(user_broadcast.user_payload),
25+
"type" => "broadcast"
26+
}
27+
28+
payload =
29+
if user_broadcast.metadata do
30+
Map.put(payload, "meta", user_broadcast.metadata)
31+
else
32+
payload
33+
end
34+
35+
{:ok, %Broadcast{event: "broadcast", payload: payload, topic: user_broadcast.topic}}
36+
end
37+
38+
def convert_to_json_broadcast(%__MODULE__{}), do: {:error, "User payload encoding is not JSON"}
39+
end

0 commit comments

Comments
 (0)