Skip to content

Commit

Permalink
serve both raw 128-bit ids and base-62 encoded
Browse files Browse the repository at this point in the history
  • Loading branch information
d2fn committed Aug 25, 2011
1 parent 6c5ce43 commit 9cee2e7
Show file tree
Hide file tree
Showing 19 changed files with 477 additions and 109 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
ebin/
.eunit/
*.dump
deps/
rel/flake/
.DS_Store

15 changes: 15 additions & 0 deletions apps/flake/src/flake.app.src
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{application, flake,
[
{description, "flake"},
{vsn, git},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
crypto,
syslog
]},
{mod, { flake_app, []}},
{env, []}
]}.
41 changes: 41 additions & 0 deletions apps/flake/src/flake.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-module (flake).
-author ('Dietrich Featherston <[email protected]>').
%%====================================================================
%% API
%%====================================================================
-export([start/0, start_link/0, stop/0]).
-export([get_config_value/2]).

-include("flake.hrl").

-include_lib("eunit/include/eunit.hrl").

ensure_started(App) ->
case application:start(App) of
ok ->
ok;
{error, {already_started, App}} ->
ok
end.

%% @spec start_link() -> {ok,Pid::pid()}
%% @doc Starts the app for inclusion in a supervisor tree
start_link() ->
flake_sup:start_link().

%% @spec start() -> ok
%% @doc Start the snowflake server.
start() ->
application:start(flake).

%% @spec stop() -> ok
%% @doc Stop the snowflake server.
stop() ->
Res = application:stop(flake),
Res.

get_config_value(Key, Default) ->
case application:get_env(flake, Key) of
{ok, Value} -> Value;
_ -> Default
end.
15 changes: 15 additions & 0 deletions apps/flake/src/flake.hrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-ifdef(TEST).
-define(LOG_INFO(F), _ = [F]).
-define(LOG_ERROR(F), _ = [F]).
-define(LOG_INFO_FORMAT(F, A), _ = [F, A]).
-define(LOG_ERROR_FORMAT(F, A), _ = [F, A]).
-else.
-define(LOG_INFO(F),
error_logger:info_msg(F)).
-define(LOG_ERROR(F),
error_logger:error_msg(F)).
-define(LOG_INFO_FORMAT(F, A),
error_logger:info_msg(F, A)).
-define(LOG_ERROR_FORMAT(F, A),
error_logger:error_msg(F, A)).
-endif.
19 changes: 19 additions & 0 deletions apps/flake/src/flake_app.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%% @doc Callbacks for the snowflake application.

-module (flake_app).
-author('Dietrich Featherston <[email protected]>').

-behaviour(application).
-export([start/2,stop/1]).

-include_lib("eunit/include/eunit.hrl").

%% @spec start(_Type, _StartArgs) -> ServerRet
%% @doc application start callback for snowflake.
start(_Type, _StartArgs) ->
flake_sup:start_link().

%% @spec stop(_State) -> ServerRet
%% @doc application stop callback for snowflake.
stop(_State) ->
ok.
13 changes: 13 additions & 0 deletions apps/flake/src/flake_harness.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-module (flake_harness).
-author ('Dietrich Featherston <[email protected]>').
-export ([timed_generate_ids/1,generate_ids/1]).
-include_lib("eunit/include/eunit.hrl").

timed_generate_ids(N) ->
?debugTime("generating ids",generate_ids(N)).

generate_ids(0) ->
ok;
generate_ids(N) ->
{ok,_Flake} = flake_server:id(62),
generate_ids(N-1).
115 changes: 115 additions & 0 deletions apps/flake/src/flake_server.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
-module (flake_server).
-behaviour (gen_server).
-export ([start_link/1]).
-export ([id/0,id/1]).
-export ([gen_id/3]).
-export ([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
-record (state, {max_time,worker_id,sequence}).

-include_lib("eunit/include/eunit.hrl").

%% ----------------------------------------------------------
%% API
%% ----------------------------------------------------------

% start and link to a new flake id generator
start_link(Config) ->
gen_server:start_link({local,flake},?MODULE,Config,[]).

% generate a new snowflake id
id() ->
respond(gen_server:call(flake,get)).

id(Base) ->
respond(gen_server:call(flake,{get,Base})).

respond({ok,Flake}) ->
{ok,Flake};
respond(X) ->
X.


%% ----------------------------------------------------------
%% gen_server callbacks
%% ----------------------------------------------------------

% Example config
% [ {worker_id, int()} ]
%

init([{worker_id,WorkerId}]) ->
{ok,#state{max_time=curr_time(),worker_id=WorkerId,sequence=0}}.

handle_call(get, _From, State = #state{max_time=MaxTime,worker_id=WorkerId,sequence=Sequence}) ->
{Resp,S0} = get(curr_time(),MaxTime,WorkerId,Sequence,State),
{reply,Resp,S0};

handle_call({get,Base}, _From, State = #state{max_time=MaxTime,worker_id=WorkerId,sequence=Sequence}) ->
{Resp,S0} = get(curr_time(),MaxTime,WorkerId,Sequence,State),
case Resp of
{ok,Id} ->
{reply,{ok,flake_util:as_list(Id,Base)},S0};
E ->
{reply,E,S0}
end;

handle_call(X, _From, State) ->
error_logger:error_msg("unrecognized msg in ~p:handle_call -> ~p~n",[?MODULE,X]),
{reply,ok,State}.

handle_cast(_, State) -> {noreply, State}.

handle_info(_, State) -> {noreply, State}.

terminate(_Reason, _State) -> ok.

code_change(_, State, _) -> {ok, State}.

% clock hasn't moved, increment sequence
get(Time,Time,WorkerId,Seq0,State) ->
Sequence = Seq0 + 1,
{{ok,gen_id(Time,WorkerId,Sequence)},State#state{sequence=Sequence}};
% clock has progressed, reset sequence
get(CurrTime,MaxTime,WorkerId,_,State) when CurrTime > MaxTime ->
{{ok,gen_id(CurrTime,WorkerId,0)},State#state{max_time=CurrTime,sequence=0}};
% clock is running backwards
get(CurrTime,MaxTime,_WorkerId,_Sequence,State) when MaxTime > CurrTime ->
{{error,clock_running_backwards},State}.


%% ----------------------------------------------------------
%% helpers
%% ----------------------------------------------------------

curr_time() ->
{MegaSec,Sec,MicroSec} = erlang:now(),
1000000000*MegaSec + Sec*1000 + erlang:trunc(MicroSec/1000).

gen_id(Time,WorkerId,Sequence) ->
( Time bsl 64 ) + ( WorkerId bsl 16 ) + Sequence.


%% ----------------------------------------------------------
%% tests
%% ----------------------------------------------------------

flake_test() ->
TS = curr_time(),
Worker = flake_util:hw_addr_to_int(lists:seq(1,6)),
Flake = gen_id(TS,Worker,0),
?debugVal(Flake),
<<Time:64/unsigned-integer,WorkerId:48/unsigned-integer,Sequence:16/unsigned-integer>> = <<Flake:128/unsigned-integer>>,
?assert(?debugVal(Time) =:= TS),
?assert(?debugVal(Worker) =:= WorkerId),
?assert(?debugVal(Sequence) =:= 0),
?debugVal(flake_util:as_list(Flake,62)),
test_gen_server(),
ok.

test_gen_server() ->
Worker = flake_util:hw_addr_to_int(lists:seq(1,6)),
{ok,FlakeServer} = flake_server:start_link([{worker_id,Worker}]),
% register(flake,FlakeServer),
?debugVal(flake_server:id()),
?debugVal(flake_server:id(62)),
ok.
60 changes: 60 additions & 0 deletions apps/flake/src/flake_sup.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
-module (flake_sup).
-author('Dietrich Featherston <[email protected]>').
-include("flake.hrl").

-include_lib("eunit/include/eunit.hrl").

-define (DEBUG,debug).

-behaviour(supervisor).

%% External exports
-export([start_link/0, upgrade/0]).

%% supervisor callbacks
-export([init/1]).

%% @spec start_link() -> ServerRet
%% @doc API for starting the supervisor.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% @spec upgrade() -> ok
%% @doc Add processes if necessary.
upgrade() ->
{ok, {_, Specs}} = init([]),

Old = sets:from_list([Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]),
New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]),
Kill = sets:subtract(Old, New),

sets:fold(
fun(Id, ok) ->
supervisor:terminate_child(?MODULE, Id),
supervisor:delete_child(?MODULE, Id),
ok
end, ok, Kill),

[supervisor:start_child(?MODULE, Spec) || Spec <- Specs],
ok.

%% @spec init([]) -> SupervisorTree
%% @doc supervisor callback.
init([]) ->
If = flake:get_config_value("interface","en0"),
{ok,WorkerId} = flake_util:get_if_hw_int(If),
Config = [
{worker_id,WorkerId}
],
Flake = {flake,
{flake_server,start_link,[Config]},
permanent, 5000, worker, [flake_server]},

% SyslogOpts = flake:get_config_value(syslog, [{hostname, "syslog.cid1.boundary.com"}, {facility, local5}]),
% Syslog = {syslog, {syslog, start_link, [flake,
% flake,
% proplists:get_value(hostname, SyslogOpts),
% 514,
% proplists:get_value(facility, SyslogOpts)]}, permanent, 2000, worker, [syslog]},
Processes = [Flake],
{ok, { {one_for_one, 10, 10}, Processes} }.
66 changes: 66 additions & 0 deletions apps/flake/src/flake_util.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
-module (flake_util).
-export ([as_list/2]).
-export ([get_if_hw_int/1,hw_addr_to_int/1]).

% get the mac/hardware address of the given interface as a 48-bit integer
get_if_hw_int(undefined) ->
{error,if_not_found};
get_if_hw_int(IfName) ->
{ok,IfAddrs} = inet:getifaddrs(),
IfProps = proplists:get_value(IfName,IfAddrs),
case IfProps of
undefined ->
{error,if_not_found};
_ ->
HwAddr = proplists:get_value(hwaddr,IfProps),
{ok,hw_addr_to_int(HwAddr)}
end.

% convert an array of 6 bytes into a 48-bit integer
hw_addr_to_int(HwAddr) ->
<<WorkerId:48/unsigned-integer>> = erlang:list_to_binary(HwAddr),
WorkerId.

%%
% n.b. - unique_id_62/0 and friends pulled from riak
%%

%% @spec integer_to_list(Integer :: integer(), Base :: integer()) ->
%% string()
%% @doc Convert an integer to its string representation in the given
%% base. Bases 2-62 are supported.
as_list(I, 10) ->
erlang:integer_to_list(I);
as_list(I, Base)
when is_integer(I),
is_integer(Base),
Base >= 2,
Base =< 1+$Z-$A+10+1+$z-$a ->
if
I < 0 ->
[$-|as_list(-I, Base, [])];
true ->
as_list(I, Base, [])
end;
as_list(I, Base) ->
erlang:error(badarg, [I, Base]).

%% @spec integer_to_list(integer(), integer(), stringing()) -> string()
as_list(I0, Base, R0) ->
D = I0 rem Base,
I1 = I0 div Base,
R1 =
if
D >= 36 ->
[D-36+$a|R0];
D >= 10 ->
[D-10+$A|R0];
true ->
[D+$0|R0]
end,
if
I1 =:= 0 ->
R1;
true ->
as_list(I1, Base, R1)
end.
12 changes: 10 additions & 2 deletions rebar.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{require_otp_vsn, "R14"}.
{deps, []}.
%%-*- mode: erlang -*-
{sub_dirs, ["apps/flake", "rel", "deps"]}.

{deps,
[
{syslog, ".*", {git, "git://github.com/JacobVorreuter/erlang_syslog.git", "master"}}
]
}.

{erl_opts, [debug_info]}.
{cover_enabled, false}.
Loading

0 comments on commit 9cee2e7

Please sign in to comment.