defmodule Mongo.IdServer do @moduledoc false # An ObjectId consists of a machine id, process id, seconds since unix epoch # and a counter. The counter is used to differentiate between generated # ObjectIds during a single second. # # A counter is generated for each second in an hour, the counter is # initialized to a random number based on MongoDB documentation's # recommendation. Each time a new ObjectId is generated we take the counter # for the current second and increment it. # # To keep the counters random and to make sure they don't grow infinitely they # need to be reset. Care needs to be taken to ensure a counter is not reset # during its second's window during which it is being used. Once each minute # ~60 counters should be reset, only counters that will be used ~30 minutes in # the future are reset to ensure the current second's counter is not touched. use GenServer @name __MODULE__ @num_counters 3600 @reset_timer 60_000 @counter_max 16_777_216 @gs_epoch :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) def start_link(_ \\ nil) do GenServer.start_link(__MODULE__, [], name: @name) end def init([]) do @name = :ets.new(@name, [:named_table, :public, write_concurrency: true]) true = :ets.insert(@name, machineprocid: {machine_id(), process_id()}) true = :ets.insert(@name, gen_counters(0..@num_counters)) _ = Process.send_after(self(), :reset_counters, @reset_timer) {:ok, opposite_on_window(:calendar.universal_time())} end def handle_info(:reset_counters, last_reset) do new_reset = opposite_on_window(:calendar.universal_time()) step = case last_reset + 1 <= new_reset do true -> 1 false -> -1 end :ets.insert(@name, gen_counters((last_reset + 1)..new_reset//step)) Process.send_after(self(), :reset_counters, @reset_timer) {:noreply, new_reset} end def new() do {machine_id, proc_id} = :ets.lookup_element(@name, :machineprocid, 2) now = :calendar.universal_time() secs = :calendar.datetime_to_gregorian_seconds(now) - @gs_epoch counter = :ets.update_counter(@name, in_window(now), 1) counter = rem(counter, @counter_max) BSON.ObjectId.new(machine_id, proc_id, secs, counter) end defp gen_counters(range) do for ix <- range do {ix, :rand.uniform(@counter_max) - 1} end end defp in_window(now) do secs = :calendar.datetime_to_gregorian_seconds(now) window = @num_counters rem(secs, window) end defp opposite_on_window(now) do secs = :calendar.datetime_to_gregorian_seconds(now) window = @num_counters half_window = div(window, 2) rem(secs + half_window, window) end defp machine_id() do {:ok, hostname} = :inet.gethostname() <> = :crypto.hash(:md5, hostname) machine_id end defp process_id() do :os.getpid() |> List.to_integer() end end