Maintenance
MyApp.Shards.maintenance(:shard_2, :drain) # stop routing, keep pools
MyApp.Shards.maintenance(:shard_2, :stop) # stop routing, then stop pools
MyApp.Shards.maintenance(:shard_1, :drain, role: :replica) # one role only
MyApp.Shards.activate(:shard_2) # start pools, then route
MyApp.Shards.status(:shard_2)
#=> {:ok, %{status: :active, roles: %{primary: :active}}}
MyApp.Shards.shards() # includes meta.pidsCalls are idempotent. Taking a shard out publishes the new status before
stopping pools; activating starts pools before publishing. Calls already
in flight are not tracked: :stop relies on the pool's own shutdown.
Adding and removing shards
MyApp.Shards.add_shard(:shard_9, primary: {Shardex.Adapter.Ecto, repo: MyApp.Repo, config: [database: "s9"]})
# warm up first: pools start, but the shard is not routed until activate/1
MyApp.Shards.add_shard(:shard_10, {Shardex.Adapter.Ecto, repo: MyApp.Repo, config: [database: "s10"]},
status: :drain
)
MyApp.Shards.remove_shard(:shard_9)With Hash, adding or removing a shard remaps most keys (a warning is
logged). With JumpHash, appending moves ~1/n keys to the new shard. With a
directory strategy, nothing moves until you assign keys to the new shard.
Persistence
Runtime changes live in memory. If the instance restarts, the boot config wins. Keep your source of truth (database, config service) and re-apply it at boot and when it changes:
for %{shard: shard, mode: mode} <- MyApp.Metadata.shards_in_maintenance() do
MyApp.Shards.maintenance(shard, mode)
endThe Coordinator can also restart on its own after a crash. It then rebuilds
from the boot config: drained or stopped shards come back :active and shards
added or removed at runtime are reverted. Shardex emits
[:shardex, :coordinator, :init] after every successful boot, so attach a
handler that re-applies your source of truth. The handler runs inside the
Coordinator while it boots, so do the work in another process:
defmodule MyApp.ShardexReapply do
def attach do
:telemetry.attach("myapp-shardex-reapply", [:shardex, :coordinator, :init], &__MODULE__.handle/4, nil)
end
def handle(_event, _measurements, %{instance: MyApp.Shards}, _config) do
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
for %{shard: shard, mode: mode} <- MyApp.Metadata.shards_in_maintenance() do
MyApp.Shards.maintenance(shard, mode)
end
end)
end
def handle(_event, _measurements, _metadata, _config), do: :ok
endCall MyApp.ShardexReapply.attach() before the instance starts (e.g. in
Application.start/2) so it also covers the initial boot.
A pool that keeps failing to start also affects restarts: if the Coordinator
crashes and cannot rebuild (with start_failure: :raise, any boot-config pool
failing to start stops it), the instance supervisor keeps restarting it until
its restart intensity is exhausted, and then the whole instance goes down.