defmodule Mongo.ReadPreference do import Keywords @moduledoc ~S""" Determines which servers are considered suitable for read operations A read preference consists of a mode and optional `tag_sets`, max_staleness_ms, and `hedge`. The mode prioritizes between primaries and secondaries to produce either a single suitable server or a list of candidate servers. If tag_sets and maxStalenessSeconds are set, they determine which candidate servers are eligible for selection. If hedge is set, it configures how server hedged reads are used. The default mode is `:primary`. The default tag_sets is a list with an empty tag set: [{}]. The default max_staleness_ms is unset. The default hedge is unset. ## mode * `:primary` Only an available primary is suitable. * `:secondary` All secondaries (and only secondaries) are candidates, but only eligible candidates (i.e. after applying tag_sets and maxStalenessSeconds) are suitable. * `:primary_preferred` If a primary is available, only the primary is suitable. Otherwise, all secondaries are candidates, but only eligible secondaries are suitable. * `:secondary_preferred` All secondaries are candidates. If there is at least one eligible secondary, only eligible secondaries are suitable. Otherwise, when there are no eligible secondaries, the primary is suitable. * `:nearest` The primary and all secondaries are candidates, but only eligible candidates are suitable. """ @type t :: %{ mode: :primary | :secondary | :primary_preferred | :secondary_preferred | :nearest, tag_sets: [%{String.t() => String.t()}], max_staleness_ms: non_neg_integer, hedge: BSON.document() } @primary %{ mode: :primary, tag_sets: [], max_staleness_ms: 0 } def primary(map \\ nil) def primary(map) when is_map(map) do Map.merge(@primary, map) end def primary(_), do: @primary @doc """ Add read preference to the cmd """ def add_read_preference(cmd, opts) do case Keyword.get(opts, :read_preference) do nil -> cmd pref -> cmd ++ ["$readPreference": pref] end end @doc """ From the specs: Use of slaveOk There are two usages of slaveOK: * A driver query parameter that predated read preference modes and tag set lists. * A wire protocol flag on OP_QUERY operations """ def slave_ok(%{:mode => :primary}) do %{:mode => :primary} end def slave_ok(config) do mode = case config[:mode] do :primary_preferred -> :primaryPreferred :secondary_preferred -> :secondaryPreferred other -> other end filter_nils(mode: mode, tag_sets: config[:tag_sets]) end ## # Therefore, when sending queries to a mongos, the following rules apply: # # For mode 'primary', drivers MUST NOT set the slaveOK wire protocol flag and MUST NOT use $readPreference def mongos(%{mode: :primary}) do nil end # For mode 'secondary', drivers MUST set the slaveOK wire protocol flag and MUST also use $readPreference def mongos(%{mode: :secondary} = config) do transform(config) end # For mode 'primaryPreferred', drivers MUST set the slaveOK wire protocol flag and MUST also use $readPreference def mongos(%{mode: :primary_preferred} = config) do transform(config) end # For mode 'secondaryPreferred', drivers MUST set the slaveOK wire protocol flag. If the read preference contains a # non-empty tag_sets parameter, maxStalenessSeconds is a positive integer, or the hedge parameter is non-empty, # drivers MUST use $readPreference; otherwise, drivers MUST NOT use $readPreference def mongos(%{mode: :secondary_preferred} = config) do transform(config) end # For mode 'nearest', drivers MUST set the slaveOK wire protocol flag and MUST also use $readPreference def mongos(%{mode: :nearest} = config) do transform(config) end defp transform(config) do mode = case config[:mode] do :primary_preferred -> :primaryPreferred :secondary_preferred -> :secondaryPreferred other -> other end max_staleness_seconds = case config[:max_staleness_ms] do i when is_integer(i) -> div(i, 1000) nil -> nil end [mode: mode, tag_sets: config[:tag_sets], maxStalenessSeconds: max_staleness_seconds, hedge: config[:hedge]] |> filter_nils() end end