Skip to content

Commit 7047d5e

Browse files
committed
Add app that tests txid matching
1 parent 7c4c758 commit 7047d5e

File tree

19 files changed

+603
-1
lines changed

19 files changed

+603
-1
lines changed

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ locals_without_parens = [
99
locals_without_parens: locals_without_parens,
1010
export: [locals_without_parens: locals_without_parens],
1111
import_deps: [:plug, :phoenix, :ecto],
12-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
12+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "apps/**/*.{ex,exs}"]
1313
]

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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Config
2+
3+
# connection_opts = [
4+
# username: "postgres",
5+
# password: "password",
6+
# hostname: "localhost",
7+
# database: "txid_sync",
8+
# port: 55555
9+
# ]
10+
11+
connection_opts = [
12+
username: "garry",
13+
password: "password",
14+
hostname: "localhost",
15+
database: "txid_sync",
16+
port: 5432
17+
]
18+
19+
config :txid_match, Postgrex, connection_opts
20+
21+
config :phoenix_sync,
22+
env: config_env(),
23+
mode: :embedded,
24+
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
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule TxidMatch.Application do
2+
# See https://hexdocs.pm/elixir/Application.html
3+
# for more information on OTP Applications
4+
@moduledoc false
5+
6+
use Application
7+
8+
@impl true
9+
def start(_type, _args) do
10+
Logger.put_application_level(:electric, :warning)
11+
Logger.put_application_level(:electric_client, :warning)
12+
13+
children = [
14+
TxidMatch.Expectations,
15+
{Postgrex,
16+
Application.get_env(:txid_match, Postgrex, [])
17+
|> Keyword.merge(name: TxidMatch.Postgrex, ssl: false, pool_size: 500)},
18+
TxidMatch.Migrator,
19+
TxidMatch.Reader,
20+
{TxidMatch.Writer.Supervisor,
21+
Application.get_env(:txid_match, TxidMatch.Writer.Supervisor, [])}
22+
]
23+
24+
# See https://hexdocs.pm/elixir/Supervisor.html
25+
# for other strategies and supported options
26+
opts = [strategy: :one_for_one, name: TxidMatch.Supervisor]
27+
Supervisor.start_link(children, opts)
28+
end
29+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule TxidMatch.Bumper do
2+
def child_spec(id) when is_integer(id) do
3+
%{
4+
id: {__MODULE__, id},
5+
start: {Task, :start_link, [__MODULE__, :run, [id]]},
6+
type: :worker,
7+
restart: :permanent
8+
}
9+
end
10+
11+
def run(id) do
12+
DBConnection.run(
13+
TxidMatch.Postgrex,
14+
fn conn ->
15+
query =
16+
Postgrex.prepare!(
17+
conn,
18+
"bump",
19+
"SELECT pg_current_xact_id()"
20+
)
21+
22+
loop(conn, query, id)
23+
end,
24+
timeout: :infinity
25+
)
26+
end
27+
28+
defp loop(conn, query, id) do
29+
%{rows: [[_txid]]} = DBConnection.execute!(conn, query, [])
30+
31+
loop(conn, query, id)
32+
end
33+
end

0 commit comments

Comments
 (0)