Current section
Files
Jump to
Current section
Files
lib/mongo/id_server.ex
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 16777216 @gs_epoch :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) def start_link do GenServer.start_link(__MODULE__, [], name: @name) end def init([]) do :random.seed(:os.timestamp) :ets.new(@name, [:named_table, :public, write_concurrency: true]) :ets.insert(@name, [machineprocid: {machine_id(), process_id()}]) :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) :ets.insert(@name, gen_counters(last_reset+1..new_reset)) 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, :random.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 <<machine_id::unsigned-big-24, _::binary>> = :crypto.hash(:md5, hostname) machine_id end defp process_id do :os.getpid |> List.to_integer endend