| % Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| % use this file except in compliance with the License. You may obtain a copy of |
| % the License at |
| % |
| % http://www.apache.org/licenses/LICENSE-2.0 |
| % |
| % Unless required by applicable law or agreed to in writing, software |
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| % License for the specific language governing permissions and limitations under |
| % the License. |
| -module(couch_uuids). |
| -include_lib("couch/include/couch_db.hrl"). |
| |
| -behaviour(gen_server). |
| -vsn(3). |
| -behaviour(config_listener). |
| |
| -export([start/0, stop/0]). |
| -export([new/0, random/0]). |
| |
| -export([init/1, terminate/2, code_change/3]). |
| -export([handle_call/3, handle_cast/2, handle_info/2]). |
| |
| % config_listener api |
| -export([handle_config_change/5, handle_config_terminate/3]). |
| |
| -define(RELISTEN_DELAY, 5000). |
| |
| start() -> |
| gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
| |
| stop() -> |
| gen_server:cast(?MODULE, stop). |
| |
| new() -> |
| gen_server:call(?MODULE, create). |
| |
| random() -> |
| list_to_binary(couch_util:to_hex(crypto:strong_rand_bytes(16))). |
| |
| init([]) -> |
| ok = config:listen_for_changes(?MODULE, nil), |
| {ok, state()}. |
| |
| terminate(_Reason, _State) -> |
| ok. |
| |
| handle_call(create, _From, random) -> |
| {reply, random(), random}; |
| handle_call(create, _From, {utc_random, ClockSeq}) -> |
| {UtcRandom, NewClockSeq} = utc_random(ClockSeq), |
| {reply, UtcRandom, {utc_random, NewClockSeq}}; |
| handle_call(create, _From, {utc_id, UtcIdSuffix, ClockSeq}) -> |
| Now = os:timestamp(), |
| {UtcId, NewClockSeq} = utc_suffix(UtcIdSuffix, ClockSeq, Now), |
| {reply, UtcId, {utc_id, UtcIdSuffix, NewClockSeq}}; |
| handle_call(create, _From, {sequential, Pref, Seq}) -> |
| Result = ?l2b(Pref ++ io_lib:format("~6.16.0b", [Seq])), |
| case Seq >= 16#fff000 of |
| true -> |
| {reply, Result, {sequential, new_prefix(), inc()}}; |
| _ -> |
| {reply, Result, {sequential, Pref, Seq + inc()}} |
| end. |
| |
| handle_cast(change, _State) -> |
| {noreply, state()}; |
| handle_cast(stop, State) -> |
| {stop, normal, State}; |
| handle_cast(_Msg, State) -> |
| {noreply, State}. |
| |
| handle_info(restart_config_listener, State) -> |
| ok = config:listen_for_changes(?MODULE, nil), |
| {noreply, State}; |
| handle_info(_Info, State) -> |
| {noreply, State}. |
| |
| code_change(_OldVsn, State, _Extra) -> |
| {ok, State}. |
| |
| handle_config_change("uuids", _, _, _, _) -> |
| {ok, gen_server:cast(?MODULE, change)}; |
| handle_config_change(_, _, _, _, _) -> |
| {ok, nil}. |
| |
| handle_config_terminate(_, stop, _) -> |
| ok; |
| handle_config_terminate(_Server, _Reason, _State) -> |
| gen_server:cast(?MODULE, change), |
| erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener). |
| |
| new_prefix() -> |
| couch_util:to_hex((crypto:strong_rand_bytes(13))). |
| |
| inc() -> |
| couch_rand:uniform(16#ffd). |
| |
| state() -> |
| AlgoStr = config:get("uuids", "algorithm", "random"), |
| case couch_util:to_existing_atom(AlgoStr) of |
| random -> |
| random; |
| utc_random -> |
| ClockSeq = micros_since_epoch(os:timestamp()), |
| {utc_random, ClockSeq}; |
| utc_id -> |
| ClockSeq = micros_since_epoch(os:timestamp()), |
| UtcIdSuffix = config:get("uuids", "utc_id_suffix", ""), |
| {utc_id, UtcIdSuffix, ClockSeq}; |
| sequential -> |
| {sequential, new_prefix(), inc()}; |
| Unknown -> |
| throw({unknown_uuid_algorithm, Unknown}) |
| end. |
| |
| micros_since_epoch({_, _, Micro} = Now) -> |
| Nowish = calendar:now_to_universal_time(Now), |
| Nowsecs = calendar:datetime_to_gregorian_seconds(Nowish), |
| Then = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), |
| (Nowsecs - Then) * 1000000 + Micro. |
| |
| utc_random(ClockSeq) -> |
| Suffix = couch_util:to_hex(crypto:strong_rand_bytes(9)), |
| utc_suffix(Suffix, ClockSeq, os:timestamp()). |
| |
| utc_suffix(Suffix, ClockSeq, Now) -> |
| OsMicros = micros_since_epoch(Now), |
| NewClockSeq = if |
| OsMicros =< ClockSeq -> |
| % Timestamp is lagging, use ClockSeq as Timestamp |
| ClockSeq + 1; |
| OsMicros > ClockSeq -> |
| % Timestamp advanced, use it, and reset ClockSeq with it |
| OsMicros |
| end, |
| Prefix = io_lib:format("~14.16.0b", [NewClockSeq]), |
| {list_to_binary(Prefix ++ Suffix), NewClockSeq}. |
| |
| |
| -ifdef(TEST). |
| |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| |
| utc_id_time_does_not_advance_test() -> |
| % Timestamp didn't advance but local clock sequence should and new UUIds |
| % should be generated |
| Now = {0, 1, 2}, |
| ClockSeq0 = micros_since_epoch({3, 4, 5}), |
| {UtcId0, ClockSeq1} = utc_suffix("", ClockSeq0, Now), |
| ?assert(is_binary(UtcId0)), |
| ?assertEqual(ClockSeq0 + 1, ClockSeq1), |
| {UtcId1, ClockSeq2} = utc_suffix("", ClockSeq1, Now), |
| ?assertNotEqual(UtcId0, UtcId1), |
| ?assertEqual(ClockSeq1 + 1, ClockSeq2). |
| |
| |
| utc_id_time_advanced_test() -> |
| % Timestamp advanced, a new UUID generated and also the last clock sequence |
| % is updated to that timestamp. |
| Now0 = {0, 1, 2}, |
| ClockSeq0 = micros_since_epoch({3, 4, 5}), |
| {UtcId0, ClockSeq1} = utc_suffix("", ClockSeq0, Now0), |
| ?assert(is_binary(UtcId0)), |
| ?assertEqual(ClockSeq0 + 1, ClockSeq1), |
| Now1 = {9, 9, 9}, |
| {UtcId1, ClockSeq2} = utc_suffix("", ClockSeq1, Now1), |
| ?assert(is_binary(UtcId1)), |
| ?assertNotEqual(UtcId0, UtcId1), |
| ?assertEqual(micros_since_epoch(Now1), ClockSeq2). |
| |
| utc_random_test_time_does_not_advance_test() -> |
| {MSec, Sec, USec} = os:timestamp(), |
| Future = {MSec + 10, Sec, USec}, |
| ClockSeqFuture = micros_since_epoch(Future), |
| {UtcRandom, NextClockSeq} = utc_random(ClockSeqFuture), |
| ?assert(is_binary(UtcRandom)), |
| ?assertEqual(32, byte_size(UtcRandom)), |
| ?assertEqual(ClockSeqFuture + 1, NextClockSeq). |
| |
| utc_random_test_time_advance_test() -> |
| ClockSeqPast = micros_since_epoch({1, 1, 1}), |
| {UtcRandom, NextClockSeq} = utc_random(ClockSeqPast), |
| ?assert(is_binary(UtcRandom)), |
| ?assertEqual(32, byte_size(UtcRandom)), |
| ?assert(NextClockSeq > micros_since_epoch({1000, 0, 0})). |
| |
| |
| -endif. |