-
-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multitenancy support #8
Changes from 14 commits
1aa2b23
92065b1
e0c90d8
c7f8934
59548b9
b4dc751
53a9363
6b1cdf9
9f00f20
f9f24f9
83953ae
9bbf7a8
167c6cb
2e6e4fb
03d8d21
6fc8ce8
7654d07
46f2cc0
b5cfff5
83f6bf0
16fde9e
e11ce74
0e36e49
b44c1e7
9b611db
9866d81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,23 @@ | ||
.PHONY: dev | ||
dev: | ||
MIX_ENV=dev ERL_AFLAGS="-kernel shell_history enabled" iex -S mix phx.server | ||
MIX_ENV=dev DB_ENC_KEY="1234567890123456" API_JWT_SECRET=dev ERL_AFLAGS="-kernel shell_history enabled" iex -S mix phx.server | ||
|
||
db_start: | ||
docker-compose -f ./dev/docker-compose.db.yml up | ||
|
||
db_stop: | ||
docker-compose -f ./dev/docker-compose.db.yml down --remove-orphans | ||
docker-compose -f ./dev/docker-compose.db.yml down --remove-orphans | ||
|
||
db_rebuild: | ||
make db_stop | ||
docker-compose -f ./dev/docker-compose.db.yml build | ||
docker-compose -f ./dev/docker-compose.db.yml up --force-recreate --build | ||
make db_start | ||
|
||
pgbench_init: | ||
PGPASSWORD=postgres pgbench -i -h 127.0.0.1 -p 6432 -U postgres -d postgres | ||
|
||
pgbench_short: | ||
PGPASSWORD=postgres pgbench -M extended --transactions 5 --jobs 4 --client 1 -h localhost -p 7654 -U postgres postgres | ||
PGPASSWORD=postgres pgbench -M extended --transactions 5 --jobs 4 --client 1 -h localhost -p 7654 -U postgres#localhost postgres | ||
|
||
pgbench_long: | ||
PGPASSWORD=postgres pgbench -M extended --transactions 500 --jobs 5 --client 100 -h localhost -p 7654 -U postgres postgres | ||
PGPASSWORD=postgres pgbench -M extended --transactions 500 --jobs 5 --client 100 -h localhost -p 7654 -U postgres#localhost postgres |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,37 @@ | ||
defmodule PgEdge do | ||
@moduledoc """ | ||
PgEdge keeps the contexts that define your domain | ||
and business logic. | ||
@moduledoc false | ||
require Logger | ||
alias PgEdge.{Tenants, Tenants.Tenant} | ||
|
||
Contexts are also responsible for managing your data, regardless | ||
if it comes from the database, an external API or others. | ||
""" | ||
@spec start_pool(String.t()) :: {:ok, pid} | {:error, any} | ||
def start_pool(external_id) do | ||
Logger.debug("Starting pool for #{external_id}") | ||
|
||
case Tenants.get_tenant_by_external_id(external_id) do | ||
%Tenant{} = tenant -> | ||
pool_spec = [ | ||
name: pool_name(external_id), | ||
worker_module: PgEdge.DbHandler, | ||
size: tenant.pool_size, | ||
max_overflow: 0 | ||
] | ||
|
||
DynamicSupervisor.start_child( | ||
{:via, PartitionSupervisor, {PgEdge.DynamicSupervisor, self()}}, | ||
:poolboy.child_spec(:worker, pool_spec, tenant) | ||
) | ||
|
||
_ -> | ||
Logger.error("Can't find tenant with external_id #{external_id}") | ||
{:error, :tenant_not_found} | ||
end | ||
end | ||
|
||
# TODO: implement stop_pool | ||
def stop_pool(_), do: :not_implemented | ||
|
||
@spec pool_name(any) :: {:via, Registry, {PgEdge.Registry.DbPool, any}} | ||
def pool_name(external_id) do | ||
{:via, Registry, {PgEdge.Registry.DbPool, external_id}} | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,7 @@ defmodule PgEdge.ClientHandler do | |
connected: false, | ||
buffer: "", | ||
db_pid: nil, | ||
tenant: nil, | ||
state: :wait_startup_packet | ||
} | ||
) | ||
|
@@ -56,8 +57,26 @@ defmodule PgEdge.ClientHandler do | |
else | ||
hello = Client.decode_startup_packet(bin) | ||
Logger.debug("Client startup message: #{inspect(hello)}") | ||
trans.send(socket, authentication_ok()) | ||
{:noreply, %{state | state: :idle}} | ||
|
||
# TODO: rewrite case -> with | ||
case hello.payload do | ||
[["user", user_value] | _] -> | ||
case String.split(user_value, "#") do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick, is Reason is when parsing the URL in Elixir we'll get this: iex> URI.parse("postgresql://postgres#dev_tenant:no_pass@localhost:7654/postgres")
%URI{
scheme: "postgresql",
authority: "postgres",
userinfo: nil,
host: "postgres",
port: nil,
path: nil,
query: nil,
fragment: "dev_tenant:no_pass@localhost:7654/postgres"
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before, I chose "@" because it would be proper to use user@organization, but many pg clients don't support @ in username. Okay, so that means that we need to choose a character that is not in the URL syntax |
||
[_user, external_id] -> | ||
# TODO: check the response | ||
PgEdge.start_pool(external_id) | ||
trans.send(socket, authentication_ok()) | ||
{:noreply, %{state | state: :idle, tenant: external_id}} | ||
|
||
_ -> | ||
Logger.error("Can't find external_id in #{inspect(user_value)}") | ||
{:stop, :normal, state} | ||
end | ||
|
||
_ -> | ||
Logger.error("Can't find user in #{inspect(hello)}") | ||
{:stop, :normal, state} | ||
end | ||
end | ||
end | ||
|
||
|
@@ -73,12 +92,13 @@ defmodule PgEdge.ClientHandler do | |
{:noreply, state} | ||
end | ||
|
||
def handle_info({:tcp, _, bin}, %{buffer: buf, db_pid: db_pid} = state) do | ||
def handle_info({:tcp, _, bin}, %{buffer: buf, db_pid: db_pid, tenant: tenant} = state) do | ||
db_pid = | ||
if db_pid do | ||
db_pid | ||
else | ||
:poolboy.checkout(:db_sess, true, 60000) | ||
PgEdge.pool_name(tenant) | ||
|> :poolboy.checkout(true, 60000) | ||
end | ||
|
||
data = buf <> bin | ||
|
@@ -121,11 +141,13 @@ defmodule PgEdge.ClientHandler do | |
def handle_call( | ||
{:client_call, bin, ready?}, | ||
_, | ||
%{socket: socket, trans: trans, db_pid: db_pid} = state | ||
%{socket: socket, trans: trans, db_pid: db_pid, tenant: tenant} = state | ||
) do | ||
db_pid1 = | ||
if ready? do | ||
:poolboy.checkin(:db_sess, db_pid) | ||
PgEdge.pool_name(tenant) | ||
|> :poolboy.checkin(db_pid) | ||
|
||
nil | ||
else | ||
db_pid | ||
|
@@ -144,7 +166,7 @@ defmodule PgEdge.ClientHandler do | |
<<0, 0, 0, 0>>, | ||
# parameter_status,<<"application_name">>,<<"nonode@nohost">> | ||
<<83, 0, 0, 0, 35>>, | ||
<<"application_name", 0, "node@nohost", 0>>, | ||
<<"application_name", 0, "nonode@nohost", 0>>, | ||
# parameter_status,<<"client_encoding">>,<<"UTF8">> | ||
<<83, 0, 0, 0, 25>>, | ||
<<99, 108, 105, 101, 110, 116, 95, 101, 110, 99, 111, 100, 105, 110, 103, 0, 85, 84, 70, 56, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
defmodule PgEdge.Helpers do | ||
@moduledoc """ | ||
This module includes helper functions for different contexts that can't be union in one module. | ||
""" | ||
|
||
@spec encrypt!(<<_::128>>, binary()) :: binary() | ||
def encrypt!(secret_key, text) do | ||
:aes_128_ecb | ||
|> :crypto.crypto_one_time(secret_key, pad(text), true) | ||
|> Base.encode64() | ||
end | ||
|
||
@spec decrypt!(<<_::128>>, binary()) :: binary() | ||
def decrypt!(secret_key, base64_text) do | ||
crypto_text = Base.decode64!(base64_text) | ||
|
||
:aes_128_ecb | ||
|> :crypto.crypto_one_time(secret_key, crypto_text, false) | ||
|> unpad() | ||
end | ||
|
||
defp pad(data) do | ||
to_add = 16 - rem(byte_size(data), 16) | ||
data <> :binary.copy(<<to_add>>, to_add) | ||
end | ||
|
||
defp unpad(data) do | ||
to_remove = :binary.last(data) | ||
:binary.part(data, 0, byte_size(data) - to_remove) | ||
end | ||
abc3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a friendly reminder that while it is of course totally fine to configure each parameter separately, on very DB-centric projects where I would often jump between system/docker/hosted pg and elixir/psql, I found it very convenient to configure the connection through the URL:
(yup, we can configure pool_size like this too!)
Again, no need for the change, just something to keep in mind in case you weren't aware!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes it's handy to update only a single value in production, for example, only pool size. In the case of URL, it needs to copy and find all credentials.