Skip to content
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

MySQL support #42

Merged
merged 16 commits into from
Apr 13, 2019
Merged
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: 5 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ env:
- CACHE_ENABLED=false TEST_OPTS='--only integration'
- CACHE_ENABLED=true PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --exclude redis_pubsub --exclude ecto_persistence --exclude phoenix_pubsub:with_ecto --include phoenix_pubsub:with_redis --include phoenix_pubsub:true'
- CACHE_ENABLED=false PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --only integration'
- CACHE_ENABLED=true PUBSUB_BROKER=phoenix_pubsub PERSISTENCE=ecto TEST_OPTS='--no-start --exclude redis_pubsub --exclude redis_persistence --exclude phoenix_pubsub:with_redis --include phoenix_pubsub:with_ecto --include phoenix_pubsub:true --include ecto_persistence'
- CACHE_ENABLED=false PUBSUB_BROKER=phoenix_pubsub PERSISTENCE=ecto TEST_OPTS='--no-start --only integration'
- CACHE_ENABLED=true PERSISTENCE=ecto RDBMS=postgres PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --exclude redis_pubsub --exclude redis_persistence --exclude phoenix_pubsub:with_redis --include phoenix_pubsub:with_ecto --include phoenix_pubsub:true --include ecto_persistence'
- CACHE_ENABLED=false PERSISTENCE=ecto RDBMS=postgres PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --only integration'
- CACHE_ENABLED=true PERSISTENCE=ecto RDBMS=mysql PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --exclude redis_pubsub --exclude redis_persistence --exclude phoenix_pubsub:with_redis --include phoenix_pubsub:with_ecto --include phoenix_pubsub:true --include ecto_persistence'
- CACHE_ENABLED=false PERSISTENCE=ecto RDBMS=mysql PUBSUB_BROKER=phoenix_pubsub TEST_OPTS='--no-start --only integration'
matrix:
exclude:
- elixir: 1.8
Expand All @@ -37,6 +39,7 @@ matrix:
otp_release: 21.1
services:
- redis-server
- mysql
addons:
postgresql: "9.6"
script:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,9 @@ This library depends on Redis and PostgreSQL, and you'll need them installed and
To setup the test DB for the Ecto persistence tests, run:

```
$ MIX_ENV=test PERSISTENCE=ecto mix do ecto.create, ecto.migrate
MIX_ENV=test PERSISTENCE=ecto mix do ecto.create, ecto.migrate # for postgres
rm -rf _build/test/lib/fun_with_flags/
MIX_ENV=test PERSISTENCE=ecto RDBMS=mysql mix do ecto.create, ecto.migrate # for mysql
```

Then, to run all the tests:
Expand Down
6 changes: 5 additions & 1 deletion bin/console_ecto
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#!/bin/bash

PERSISTENCE=ecto PUBSUB_BROKER=phoenix_pubsub iex -S mix
rdbms=${1:-postgres}

rm -rf _build/dev/lib/fun_with_flags/ &&
rm -rf _build/test/lib/fun_with_flags/ &&
PERSISTENCE=ecto RDBMS="$rdbms" PUBSUB_BROKER=phoenix_pubsub iex -S mix;
13 changes: 11 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,20 @@ if with_ecto do
config :fun_with_flags, ecto_repos: [FunWithFlags.Dev.EctoRepo]

config :fun_with_flags, FunWithFlags.Dev.EctoRepo,
username: "postgres",
password: "postgres",
database: "fun_with_flags_dev",
hostname: "localhost",
pool_size: 10

case System.get_env("RDBMS") do
"mysql" ->
config :fun_with_flags, FunWithFlags.Dev.EctoRepo,
username: "root",
password: ""
_ ->
config :fun_with_flags, FunWithFlags.Dev.EctoRepo,
username: "postgres",
password: "postgres"
end
end

# -------------------------------------------------
Expand Down
10 changes: 9 additions & 1 deletion dev_support/ecto/repo.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
if FunWithFlags.Config.persist_in_ecto? do
defmodule FunWithFlags.Dev.EctoRepo do
use Ecto.Repo, otp_app: :fun_with_flags, adapter: Ecto.Adapters.Postgres

# Only for dev and test.
#
@variant (case System.get_env("RDBMS") do
"mysql" -> Ecto.Adapters.MySQL
_ -> Ecto.Adapters.Postgres
end)

use Ecto.Repo, otp_app: :fun_with_flags, adapter: @variant
end
end
101 changes: 93 additions & 8 deletions lib/fun_with_flags/store/persistent/ecto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ defmodule FunWithFlags.Store.Persistent.Ecto do

import Ecto.Query

require Logger

@repo Config.ecto_repo()
@mysql_lock_timeout_s 3

def worker_spec do
nil
Expand Down Expand Up @@ -39,8 +42,12 @@ defmodule FunWithFlags.Store.Persistent.Ecto do
where: r.gate_type == "percentage"
)

out = @repo.transaction fn() ->
table_lock!()
transaction_fn = case db_type() do
:postgres -> &_transaction_with_lock_postgres/1
:mysql -> &_transaction_with_lock_mysql/1
end

out = transaction_fn.(fn() ->
case @repo.one(find_one_q) do
record = %Record{} ->
changeset = Record.update_target(record, gate)
Expand All @@ -49,7 +56,8 @@ defmodule FunWithFlags.Store.Persistent.Ecto do
changeset = Record.build(flag_name, gate)
do_insert(flag_name, changeset)
end
end
end)


case out do
{:ok, {:ok, result}} ->
Expand All @@ -63,10 +71,7 @@ defmodule FunWithFlags.Store.Persistent.Ecto do

def put(flag_name, gate = %Gate{}) do
changeset = Record.build(flag_name, gate)
options = [
on_conflict: [set: [enabled: gate.enabled]],
conflict_target: [:flag_name, :gate_type, :target] # the unique index
]
options = upsert_options(gate)

case do_insert(flag_name, changeset, options) do
{:ok, flag} ->
Expand All @@ -78,6 +83,39 @@ defmodule FunWithFlags.Store.Persistent.Ecto do
end


defp _transaction_with_lock_postgres(upsert_fn) do
@repo.transaction fn() ->
postgres_table_lock!()
upsert_fn.()
end
end

defp _transaction_with_lock_mysql(upsert_fn) do
@repo.transaction fn() ->
if mysql_lock!() do
try do
upsert_fn.()
rescue
e ->
@repo.rollback("Exception: #{inspect(e)}")
else
{:error, reason} ->
@repo.rollback("Error while upserting the gate: #{inspect(reason)}")
{:ok, value} ->
{:ok, value}
after
# This is not guaranteed to run if the VM crashes, but at least the
# lock gets released when the MySQL client session is terminated.
mysql_unlock!()
end
else
Logger.error("Couldn't acquire lock with 'SELECT GET_LOCK()' after #{@mysql_lock_timeout_s} seconds")
@repo.rollback("couldn't acquire lock")
end
end
end


def delete(flag_name, %Gate{type: type})
when type in [:percentage_of_time, :percentage_of_actors] do
name_string = to_string(flag_name)
Expand Down Expand Up @@ -180,14 +218,61 @@ defmodule FunWithFlags.Store.Persistent.Ecto do
end


defp table_lock! do
defp postgres_table_lock! do
Ecto.Adapters.SQL.query!(
@repo,
"LOCK TABLE fun_with_flags_toggles IN SHARE ROW EXCLUSIVE MODE;"
)
end


defp mysql_lock! do
result = Ecto.Adapters.SQL.query!(
@repo,
"SELECT GET_LOCK('fun_with_flags_percentage_gate_upsert', #{@mysql_lock_timeout_s})"
)

%{rows: [[i]]} = result
i == 1
end


defp mysql_unlock! do
result = Ecto.Adapters.SQL.query!(
@repo,
"SELECT RELEASE_LOCK('fun_with_flags_percentage_gate_upsert');"
)

%{rows: [[i]]} = result
i == 1
end


# PostgreSQL's UPSERTs require an explicit conflict target.
# MySQL's UPSERTs don't need it.
#
defp upsert_options(gate = %Gate{}) do
options = [on_conflict: [set: [enabled: gate.enabled]]]

case db_type() do
:postgres ->
options ++ [conflict_target: [:flag_name, :gate_type, :target]]
:mysql ->
options
end
end


defp db_type do
case @repo.__adapter__() do
Ecto.Adapters.Postgres -> :postgres
Ecto.Adapters.MySQL -> :mysql # legacy, Mariaex
Ecto.Adapters.MyXQL -> :mysql # new in ecto_sql 3.1
other -> raise "Ecto adapter #{inspect(other)} is not supported"
end
end


defp do_insert(flag_name, changeset, options \\ []) do
changeset
|> @repo.insert(options)
Expand Down
2 changes: 2 additions & 0 deletions lib/fun_with_flags/store/persistent/ecto/null_repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ defmodule FunWithFlags.NullEctoRepo do
def update(_, _), do: raise(@error_msg)
def delete_all(_), do: raise(@error_msg)
def transaction(_), do: raise(@error_msg)
def rollback(_), do: raise(@error_msg)
def __adapter__(), do: raise(@error_msg)
end
46 changes: 40 additions & 6 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ defmodule FunWithFlags.Mixfile do
{:redix, "~> 0.9.1", optional: true},
{:ecto_sql, "~> 3.0", optional: true},
{:postgrex, "~> 0.13", optional: true, only: [:dev, :test]},
{:mariaex, "~> 0.9.1", optional: true, only: [:dev, :test]},

{:phoenix_pubsub, "~> 1.0", optional: true},

Expand All @@ -75,10 +76,12 @@ defmodule FunWithFlags.Mixfile do
{:"test.all", [
&run_tests__redis_pers__redis_pubsub/1, &run_integration_tests__redis_pers__redis_pubsub__no_cache/1,
&run_tests__redis_pers__phoenix_pubsub/1, &run_integration_tests__redis_pers__phoenix_pubsub__no_cache/1,
&run_tests__ecto_pers__phoenix_pubsub/1, &run_integration_tests__ecto_pers__phoenix_pubsub__no_cache/1,
&run_tests__ecto_pers_postgres__phoenix_pubsub/1, &run_integration_tests__ecto_pers_postgres__phoenix_pubsub__no_cache/1,
&run_tests__ecto_pers_mysql__phoenix_pubsub/1, &run_integration_tests__ecto_pers_mysql__phoenix_pubsub__no_cache/1,
]},
{:"test.phx", [&run_tests__redis_pers__phoenix_pubsub/1]},
{:"test.ecto", [&run_tests__ecto_pers__phoenix_pubsub/1]},
{:"test.ecto.postgres", [&run_tests__ecto_pers_postgres__phoenix_pubsub/1]},
{:"test.ecto.mysql", [&run_tests__ecto_pers_mysql__phoenix_pubsub/1]},
{:"test.redis", [&run_tests__redis_pers__redis_pubsub/1]},
]
end
Expand Down Expand Up @@ -134,29 +137,60 @@ defmodule FunWithFlags.Mixfile do
)
end

# Run the tests with Ecto as persistent store and Phoenix.PubSub as broker.
# Run the tests with Ecto+PostgreSQL as persistent store and Phoenix.PubSub as broker.
#
defp run_tests__ecto_pers__phoenix_pubsub(_) do
defp run_tests__ecto_pers_postgres__phoenix_pubsub(_) do
Mix.shell.cmd(
"mix test --color --force --no-start --exclude redis_pubsub --exclude redis_persistence --exclude phoenix_pubsub:with_redis --include phoenix_pubsub:with_ecto --include phoenix_pubsub:true --include ecto_persistence",
env: [
{"CACHE_ENABLED", "true"},
{"PUBSUB_BROKER", "phoenix_pubsub"},
{"PERSISTENCE", "ecto"},
{"RDBMS", "postgres"},
]
)
end

# Run the tests with Ecto+MySQL as persistent store and Phoenix.PubSub as broker.
#
defp run_tests__ecto_pers_mysql__phoenix_pubsub(_) do
Mix.shell.cmd(
"mix test --color --force --no-start --exclude redis_pubsub --exclude redis_persistence --exclude phoenix_pubsub:with_redis --include phoenix_pubsub:with_ecto --include phoenix_pubsub:true --include ecto_persistence",
env: [
{"CACHE_ENABLED", "true"},
{"PUBSUB_BROKER", "phoenix_pubsub"},
{"PERSISTENCE", "ecto"},
{"RDBMS", "mysql"},
]
)
end

# Runs the integration tests only.
# Cache disabled, Ecto+PostgreSQL as persistent store and Phoenix.PubSub as broker.
#
defp run_integration_tests__ecto_pers_postgres__phoenix_pubsub__no_cache(_) do
Mix.shell.cmd(
"mix test --color --force --no-start --only integration",
env: [
{"CACHE_ENABLED", "false"},
{"PUBSUB_BROKER", "phoenix_pubsub"},
{"PERSISTENCE", "ecto"},
{"RDBMS", "postgres"},
]
)
end

# Runs the integration tests only.
# Cache disabled, Ecto as persistent store and Phoenix.PubSub as broker.
# Cache disabled, Ecto+MySQL as persistent store and Phoenix.PubSub as broker.
#
defp run_integration_tests__ecto_pers__phoenix_pubsub__no_cache(_) do
defp run_integration_tests__ecto_pers_mysql__phoenix_pubsub__no_cache(_) do
Mix.shell.cmd(
"mix test --color --force --no-start --only integration",
env: [
{"CACHE_ENABLED", "false"},
{"PUBSUB_BROKER", "phoenix_pubsub"},
{"PERSISTENCE", "ecto"},
{"RDBMS", "mysql"},
]
)
end
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"mariaex": {:hex, :mariaex, "0.9.1", "83266fec657ea68dd426f4bbc12594be45ee91fe162ebf1bf017ce3cfa098ddd", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ IO.puts "--------------------------------------------------------------"
IO.puts "$TEST_OPTS='#{System.get_env("TEST_OPTS")}'"
IO.puts "$CACHE_ENABLED=#{System.get_env("CACHE_ENABLED")}"
IO.puts "$PERSISTENCE=#{System.get_env("PERSISTENCE")}"
IO.puts "$RDBMS=#{System.get_env("RDBMS")}"
IO.puts "$PUBSUB_BROKER=#{System.get_env("PUBSUB_BROKER")}"
IO.puts "--------------------------------------------------------------"
IO.puts "Cache enabled: #{inspect(FunWithFlags.Config.cache?)}"
Expand Down