Skip to content

Commit f154cac

Browse files
authored
fix: return 32-bit txids to ensure matching with sync messages (#71)
Fixes #68
1 parent 6efb7dc commit f154cac

File tree

27 files changed

+633
-30
lines changed

27 files changed

+633
-30
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
12+
- Changed type of returned txid to binary to prevent JSON serialization issues ([#71](https://github.com/electric-sql/phoenix_sync/pull/71))
13+
814
## [0.5.0] - 2025-08-13
915

1016
### Added

apps/phoenix_sync_example/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ defmodule PhoenixSyncExample.MixProject do
5050
{:dns_cluster, "~> 0.1.1"},
5151
{:bandit, "~> 1.5"},
5252
{:phoenix_sync, path: "../.."},
53-
{:electric, "~> 1.0.24"}
53+
{:electric, "~> 1.1.0"}
5454
]
5555
end
5656

apps/phoenix_sync_example/mix.lock

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

apps/txid_match/.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

apps/txid_match/.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
18+
19+
# Ignore package tarball (built via "mix hex.build").
20+
txid_match-*.tar
21+
22+
# Temporary files, for example, from tests.
23+
/tmp/

apps/txid_match/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# TxidMatch
2+
3+
Forces postgres's txid to wrap around in order to validate transaction id
4+
matching.
5+
6+
On my machine I can force a wrap around in about 8 hours. Tweak the
7+
`bumper_count` in `TxidMatch.Writer.Supervisor` to work with your machine.
8+
9+
Only tested/works on Linux.
10+
11+
# create a local postgres database with an in-memory data dir
12+
mix txid.postgres up
13+
14+
# test with txid_current() as the txid function
15+
# returns 64-bit ids and fails
16+
mix txid "txid_current()"
17+
18+
# test with txid_current() as the txid function
19+
# 32-bit ids and works
20+
mix txid "pg_current_xact_id()::xid"
21+
22+
# stop the pg server and release the ramdisk
23+
mix txid.postgres down

apps/txid_match/config/config.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Config
2+
3+
4+
connection_opts = [
5+
username: "garry",
6+
password: "password",
7+
hostname: "localhost",
8+
database: "txid_sync",
9+
port: 5432
10+
]
11+
12+
config :txid_match, Postgrex, connection_opts
13+
14+
config :phoenix_sync,
15+
env: config_env(),
16+
mode: :embedded,
17+
connection_opts: [{:sslmode, :disable} | connection_opts]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
defmodule Mix.Tasks.Txid.Postgres do
2+
use Mix.Task
3+
4+
@shortdoc "Manage the in-memory PostgreSQL database for txid_match"
5+
6+
def run(args) do
7+
{_, [action], _} = OptionParser.parse(args, strict: [])
8+
9+
case action do
10+
"up" -> postgres_up()
11+
"down" -> postgres_down()
12+
_ -> Mix.raise("Unknown action: #{action}. Use 'up' or 'down'.")
13+
end
14+
end
15+
16+
defp postgres_up do
17+
Mix.shell().info("Starting PostgreSQL in-memory database...")
18+
db_config = Application.get_env(:txid_match, TxidMatch.Postgrex, [])
19+
20+
{:ok, database} = Keyword.fetch(db_config, :database)
21+
{:ok, host} = Keyword.fetch(db_config, :hostname)
22+
{:ok, port} = Keyword.fetch(db_config, :port)
23+
{:ok, username} = Keyword.fetch(db_config, :username)
24+
{:ok, password} = Keyword.fetch(db_config, :password)
25+
26+
System.put_env("PGPASSWORD", password)
27+
data_dir = data_dir()
28+
# 2 GB
29+
disk_size = 2 * 1024 * 1024 * 1024
30+
31+
File.mkdir_p!(data_dir)
32+
user = System.get_env("USER") || raise "$USER not present"
33+
34+
{_, 0} = System.cmd("sudo", ~w(modprobe brd rd_nr=1 rd_size=#{disk_size}))
35+
{_, 0} = System.cmd("sudo", ~w(mkfs.xfs /dev/ram0))
36+
{_, 0} = System.cmd("sudo", ~w(mount /dev/ram0 #{data_dir}))
37+
{_, 0} = System.cmd("sudo", ~w(chown #{user} #{data_dir}))
38+
39+
File.write!("#{data_dir}/postgresql.conf", """
40+
listen_addresses = '*'
41+
wal_level = logical
42+
max_replication_slots = 100
43+
max_connections = 1000
44+
fsync = off
45+
""")
46+
47+
{_, 0} = System.cmd("pg_ctrl", ~w[init -D #{data_dir}])
48+
{_, 0} = System.cmd("pg_ctrl", ~w[start -D #{data_dir}])
49+
end
50+
51+
defp postgres_down do
52+
Mix.shell().info("Stopping PostgreSQL in-memory database...")
53+
data_dir = data_dir() |> dbg
54+
55+
{_, _} = System.cmd("pg_ctl", ~w[stop -D #{data_dir}])
56+
{_, 0} = System.cmd("sudo", ~w(umount #{data_dir}))
57+
{_, 0} = System.cmd("sudo", ~w(rmmod brd))
58+
59+
File.rm_rf!(data_dir)
60+
end
61+
62+
defp data_dir do
63+
System.get_env("PGDATA", Path.expand("tmp/pgdata", File.cwd!()))
64+
end
65+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
defmodule Mix.Tasks.Txid do
2+
use Mix.Task
3+
4+
@shortdoc "Run the TXID Match Task"
5+
6+
def run(args) do
7+
{_, [function], _} = OptionParser.parse(args, strict: [])
8+
9+
Mix.shell().info("Running TXID Match Task with function: #{function}")
10+
11+
Application.put_env(:txid_match, :function, function, persistent: true)
12+
13+
db_config = Application.get_env(:txid_match, Postgrex, [])
14+
15+
{:ok, database} = Keyword.fetch(db_config, :database)
16+
{:ok, host} = Keyword.fetch(db_config, :hostname)
17+
{:ok, port} = Keyword.fetch(db_config, :port)
18+
{:ok, username} = Keyword.fetch(db_config, :username)
19+
{:ok, password} = Keyword.fetch(db_config, :password)
20+
21+
System.put_env("PGPASSWORD", password)
22+
23+
{out, 0} =
24+
System.cmd("psql", [
25+
"--host",
26+
host,
27+
"--port",
28+
"#{port}",
29+
"--username",
30+
username,
31+
"--tuples-only",
32+
"--csv",
33+
"--list"
34+
])
35+
36+
dbs =
37+
out
38+
|> String.split("\n")
39+
|> Enum.map(&String.split(&1, ","))
40+
|> Enum.map(&Enum.at(&1, 0))
41+
42+
if database in dbs do
43+
{_, 0} =
44+
System.cmd("dropdb", [
45+
"--host",
46+
host,
47+
"--port",
48+
"#{port}",
49+
"--username",
50+
username,
51+
"--force",
52+
database
53+
])
54+
55+
Mix.shell().info("Dropped database #{database}")
56+
end
57+
58+
{_, 0} =
59+
System.cmd("createdb", [
60+
"-T",
61+
"template0",
62+
"-E",
63+
"UTF-8",
64+
"--host",
65+
host,
66+
"--port",
67+
"#{port}",
68+
"--username",
69+
username,
70+
database
71+
])
72+
73+
Mix.shell().info("Created database #{database}")
74+
75+
Mix.Task.run("app.start", args)
76+
77+
# # Ensure the application is started
78+
# Application.ensure_all_started(:txid_match)
79+
80+
# Run the main logic of the task
81+
IO.puts("Running TXID Match Task with arguments: #{inspect(args)}")
82+
83+
# Here you can add the logic for your task
84+
# For example, you might want to call a function from your application module
85+
# TXIDMatch.SomeModule.some_function(args)
86+
Process.sleep(:infinity)
87+
end
88+
end

apps/txid_match/lib/txid_match.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule TxidMatch do
2+
require Logger
3+
4+
def query!(sql, params \\ []) do
5+
Logger.debug("Executing SQL: #{sql} with params: #{inspect(params)}")
6+
Postgrex.query!(TxidMatch.Postgrex, sql, params)
7+
end
8+
9+
def txid do
10+
# mix txid "txid_current()"
11+
# mix txid "pg_current_xact_id()::xid"
12+
Application.fetch_env!(:txid_match, :function)
13+
end
14+
end

0 commit comments

Comments
 (0)