From d243ffca232d7cccbbfd1539230d1299e283ea17 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Tue, 31 Mar 2015 16:46:03 +0900 Subject: [PATCH 01/17] Single server client & server code (squashed) --- .gitignore | 8 +- Makefile | 38 ++ ebin/.gitignore | 2 + include/machi.hrl | 26 + include/machi_projection.hrl | 33 + rebar | Bin 0 -> 193108 bytes rebar.config | 7 + rebar.config.script | 47 ++ src/machi.app.src | 13 + src/machi_app.erl | 37 ++ src/machi_chash.erl | 459 ++++++++++++++ src/machi_flu1.erl | 464 ++++++++++++++ src/machi_flu1_client.erl | 399 ++++++++++++ src/machi_flu_sup.erl | 51 ++ src/machi_sequencer.erl | 191 ++++++ src/machi_sup.erl | 55 ++ src/machi_util.erl | 1102 ++++++++++++++++++++++++++++++++++ test/machi_flu1_test.erl | 98 +++ 18 files changed, 3027 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 ebin/.gitignore create mode 100644 include/machi.hrl create mode 100644 include/machi_projection.hrl create mode 100755 rebar create mode 100644 rebar.config create mode 100644 rebar.config.script create mode 100644 src/machi.app.src create mode 100644 src/machi_app.erl create mode 100644 src/machi_chash.erl create mode 100644 src/machi_flu1.erl create mode 100644 src/machi_flu1_client.erl create mode 100644 src/machi_flu_sup.erl create mode 100644 src/machi_sequencer.erl create mode 100644 src/machi_sup.erl create mode 100644 src/machi_util.erl create mode 100644 test/machi_flu1_test.erl diff --git a/.gitignore b/.gitignore index 2693865..180a370 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ +prototype/chain-manager/patch.* .eunit deps -*.o -ebin/*.beam *.plt erl_crash.dump -rel/example_project .concrete/DEV_MODE .rebar +doc/edoc-info +doc/erlang.png +doc/*.html +doc/stylesheet.css diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..310b8a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +REBAR_BIN := $(shell which rebar) +ifeq ($(REBAR_BIN),) +REBAR_BIN = ./rebar +endif + +.PHONY: rel deps package pkgclean + +all: deps compile + +compile: + $(REBAR_BIN) compile + +deps: + $(REBAR_BIN) get-deps + +clean: + $(REBAR_BIN) -r clean + +test: deps compile eunit + +eunit: + $(REBAR_BIN) -v skip_deps=true eunit + +pulse: compile + env USE_PULSE=1 $(REBAR_BIN) skip_deps=true clean compile + env USE_PULSE=1 $(REBAR_BIN) skip_deps=true -D PULSE eunit + +APPS = kernel stdlib sasl erts ssl compiler eunit crypto +PLT = $(HOME)/.machi_dialyzer_plt + +build_plt: deps compile + dialyzer --build_plt --output_plt $(PLT) --apps $(APPS) deps/*/ebin + +dialyzer: deps compile + dialyzer -Wno_return --plt $(PLT) ebin + +clean_plt: + rm $(PLT) diff --git a/ebin/.gitignore b/ebin/.gitignore new file mode 100644 index 0000000..120fe3a --- /dev/null +++ b/ebin/.gitignore @@ -0,0 +1,2 @@ +*.beam +*.app diff --git a/include/machi.hrl b/include/machi.hrl new file mode 100644 index 0000000..d717083 --- /dev/null +++ b/include/machi.hrl @@ -0,0 +1,26 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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. +%% +%% ------------------------------------------------------------------- + +-define(MAX_FILE_SIZE, 256*1024*1024). % 256 MBytes +-define(MAX_CHUNK_SIZE, ((1 bsl 32) - 1)). +%% -define(DATA_DIR, "/Volumes/SAM1/seq-tests/data"). +-define(DATA_DIR, "./data"). +-define(MINIMUM_OFFSET, 1024). + diff --git a/include/machi_projection.hrl b/include/machi_projection.hrl new file mode 100644 index 0000000..4b431b6 --- /dev/null +++ b/include/machi_projection.hrl @@ -0,0 +1,33 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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. +%% +%% ------------------------------------------------------------------- + +-record(projection, { + %% hard state + epoch :: non_neg_integer(), + last_epoch :: non_neg_integer(), + float_map, + last_float_map, + %% soft state + migrating :: boolean(), + tree, + last_tree + }). + +-define(SHA_MAX, (1 bsl (20*8))). diff --git a/rebar b/rebar new file mode 100755 index 0000000000000000000000000000000000000000..03c9be6c6ac439422dae9343a4648853abf00c34 GIT binary patch literal 193108 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

HC}Wsmi{mD0mC!dE25H3zWd9=w*~@4y!$c<1y6b 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 literal 0 HcmV?d00001 diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..5b3cfa2 --- /dev/null +++ b/rebar.config @@ -0,0 +1,7 @@ +%%% {erl_opts, [warnings_as_errors, {parse_transform, lager_transform}, debug_info]}. +{erl_opts, [{parse_transform, lager_transform}, debug_info]}. + +{deps, [ + {lager, ".*", {git, "git://github.com/basho/lager.git", {tag, "2.0.1"}}} + ]}. + diff --git a/rebar.config.script b/rebar.config.script new file mode 100644 index 0000000..364ce39 --- /dev/null +++ b/rebar.config.script @@ -0,0 +1,47 @@ +PulseBuild = case os:getenv("USE_PULSE") of + false -> + false; + _ -> + true + end, +case PulseBuild of + true -> + PulseOpts = + [{pulse_no_side_effect, + [{erlang,display,1} + ]}, + {pulse_side_effect, + [ {does_not_exist_yet, some_func, '_'} + + , {prim_file, '_', '_'} + , {file, '_', '_'} + , {filelib, '_', '_'} + , {os, '_', '_'} ]}, + + {pulse_replace_module, + [ {gen_server, pulse_gen_server} + , {application, pulse_application} + , {supervisor, pulse_supervisor} ]} + ], + PulseCFlags = [{"CFLAGS", "$CFLAGS -DPULSE"}], + UpdConfig = case lists:keysearch(eunit_compile_opts, 1, CONFIG) of + {value, {eunit_compile_opts, Opts}} -> + lists:keyreplace(eunit_compile_opts, + 1, + CONFIG, + {eunit_compile_opts, Opts ++ PulseOpts}); + _ -> + [{eunit_compile_opts, PulseOpts} | CONFIG] + end, + case lists:keysearch(port_env, 1, UpdConfig) of + {value, {port_env, PortEnv}} -> + lists:keyreplace(port_env, + 1, + UpdConfig, + {port_env, PortEnv ++ PulseCFlags}); + _ -> + [{port_env, PulseCFlags} | UpdConfig] + end; + false -> + CONFIG +end. diff --git a/src/machi.app.src b/src/machi.app.src new file mode 100644 index 0000000..7a2866b --- /dev/null +++ b/src/machi.app.src @@ -0,0 +1,13 @@ +{application, machi, [ + {description, "A village of write-once files."}, + {vsn, "0.0.0"}, + {applications, [kernel, stdlib, sasl, crypto]}, + {mod,{machi_app,[]}}, + {registered, []}, + {env, [ + {flu_list, + [ + {flu_a, 32900, "./data.flu_a"} + ]} + ]} +]}. diff --git a/src/machi_app.erl b/src/machi_app.erl new file mode 100644 index 0000000..6dfddf7 --- /dev/null +++ b/src/machi_app.erl @@ -0,0 +1,37 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +start(_StartType, _StartArgs) -> + case machi_sup:start_link() of + {ok, Pid} -> + {ok, Pid}; + Error -> + Error + end. + +stop(_State) -> + ok. diff --git a/src/machi_chash.erl b/src/machi_chash.erl new file mode 100644 index 0000000..f45473a --- /dev/null +++ b/src/machi_chash.erl @@ -0,0 +1,459 @@ +%%%------------------------------------------------------------------- +%%% Copyright (c) 2007-2011 Gemini Mobile Technologies, Inc. All rights reserved. +%%% Copyright (c) 2013-2015 Basho Technologies, Inc. All rights reserved. +%%% +%%% 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. +%%% +%%%------------------------------------------------------------------- + +%% Consistent hashing library. Also known as "random slicing". +%% Originally from the Hibari DB source code at https://github.com/hibari +%% +%% TODO items: +%% +%% 1. Refactor to use bigints instead of floating point numbers. The +%% ?SMALLEST_SIGNIFICANT_FLOAT_SIZE macro below doesn't allow as +%% much wiggle-room for making really small hashing range +%% definitions. + +-module(machi_chash). + +-define(SMALLEST_SIGNIFICANT_FLOAT_SIZE, 0.1e-12). +-define(SHA_MAX, (1 bsl (20*8))). + +%% -compile(export_all). +-export([make_float_map/1, make_float_map/2, + sum_map_weights/1, + make_tree/1, + query_tree/2, + hash_binary_via_float_map/2, + hash_binary_via_float_tree/2, + pretty_with_integers/2, + pretty_with_integers/3]). +-export([make_demo_map1/0, make_demo_map2/0]). +-export([zzz_usage_details/0]). % merely to give EDoc a hint of our intent + +-type owner_name() :: term(). +%% Owner for a range on the unit interval. We are agnostic about its +%% type. +-type weight() :: non_neg_integer(). +%% For this library, a weight is an integer which specifies the +%% capacity of a "owner" relative to other owners. For example, if +%% owner A with a weight of 10, and if owner B has a weight of 20, +%% then B will be assigned twice as much of the unit interval as A. + +-type float_map() :: [{owner_name(), float()}]. +%% A float map subdivides the unit interval, starting at 0.0, to +%% partitions that are assigned to various owners. The sum of all +%% floats must be exactly 1.0 (or close enough for floating point +%% purposes). + +-opaque float_tree() :: gb_trees:tree(float(), owner_name()). +%% We can't use gb_trees:tree() because 'nil' (the empty tree) is +%% never valid in our case. But teaching Dialyzer that is difficult. + +-type owner_int_range() :: {owner_name(), non_neg_integer(), non_neg_integer()}. +%% Used when "prettying" a float map. +-type owner_weight() :: {owner_name(), weight()}. + +-type owner_weight_list() :: [owner_weight()]. +%% A owner_weight_list is a definition of brick assignments over the +%% unit interval [0.0, 1.0]. The sum of all floats must be 1.0. For +%% example, [{{br1,nd1}, 0.25}, {{br2,nd1}, 0.5}, {{br3,nd1}, 0.25}]. + +-export_type([float_map/0, float_tree/0]). + +%% @doc Create a float map, based on a basic owner weight list. + +-spec make_float_map(owner_weight_list()) -> float_map(). +make_float_map(NewOwnerWeights) -> + make_float_map([], NewOwnerWeights). + +%% @doc Create a float map, based on an older float map and a new weight +%% list. +%% +%% The weights in the new weight list may be different than (or the +%% same as) whatever weights were used to make the older float map. + +-spec make_float_map(float_map(), owner_weight_list()) -> float_map(). +make_float_map([], NewOwnerWeights) -> + Sum = add_all_weights(NewOwnerWeights), + DiffMap = [{Ch, Wt/Sum} || {Ch, Wt} <- NewOwnerWeights], + make_float_map2([{unused, 1.0}], DiffMap, NewOwnerWeights); +make_float_map(OldFloatMap, NewOwnerWeights) -> + NewSum = add_all_weights(NewOwnerWeights), + %% Normalize to unit interval + %% NewOwnerWeights2 = [{Ch, Wt / NewSum} || {Ch, Wt} <- NewOwnerWeights], + + %% Reconstruct old owner weights (will be normalized to unit interval) + SumOldFloatsDict = + lists:foldl(fun({Ch, Wt}, OrdDict) -> + orddict:update_counter(Ch, Wt, OrdDict) + end, orddict:new(), OldFloatMap), + OldOwnerWeights = orddict:to_list(SumOldFloatsDict), + OldSum = add_all_weights(OldOwnerWeights), + + OldChs = [Ch || {Ch, _} <- OldOwnerWeights], + NewChs = [Ch || {Ch, _} <- NewOwnerWeights], + OldChsOnly = OldChs -- NewChs, + + %% Mark any space in by a deleted owner as unused. + OldFloatMap2 = lists:map( + fun({Ch, Wt} = ChWt) -> + case lists:member(Ch, OldChsOnly) of + true -> + {unused, Wt}; + false -> + ChWt + end + end, OldFloatMap), + + %% Create a diff map of changing owners and added owners + DiffMap = lists:map(fun({Ch, NewWt}) -> + case orddict:find(Ch, SumOldFloatsDict) of + {ok, OldWt} -> + {Ch, (NewWt / NewSum) - + (OldWt / OldSum)}; + error -> + {Ch, NewWt / NewSum} + end + end, NewOwnerWeights), + make_float_map2(OldFloatMap2, DiffMap, NewOwnerWeights). + +make_float_map2(OldFloatMap, DiffMap, _NewOwnerWeights) -> + FloatMap = apply_diffmap(DiffMap, OldFloatMap), + XX = combine_neighbors(collapse_unused_in_float_map(FloatMap)), + XX. + +apply_diffmap(DiffMap, FloatMap) -> + SubtractDiff = [{Ch, abs(Diff)} || {Ch, Diff} <- DiffMap, Diff < 0], + AddDiff = [D || {_Ch, Diff} = D <- DiffMap, Diff > 0], + TmpFloatMap = iter_diffmap_subtract(SubtractDiff, FloatMap), + iter_diffmap_add(AddDiff, TmpFloatMap). + +add_all_weights(OwnerWeights) -> + lists:foldl(fun({_Ch, Weight}, Sum) -> Sum + Weight end, 0.0, OwnerWeights). + +iter_diffmap_subtract([{Ch, Diff}|T], FloatMap) -> + iter_diffmap_subtract(T, apply_diffmap_subtract(Ch, Diff, FloatMap)); +iter_diffmap_subtract([], FloatMap) -> + FloatMap. + +iter_diffmap_add([{Ch, Diff}|T], FloatMap) -> + iter_diffmap_add(T, apply_diffmap_add(Ch, Diff, FloatMap)); +iter_diffmap_add([], FloatMap) -> + FloatMap. + +apply_diffmap_subtract(Ch, Diff, [{Ch, Wt}|T]) -> + if Wt == Diff -> + [{unused, Wt}|T]; + Wt > Diff -> + [{Ch, Wt - Diff}, {unused, Diff}|T]; + Wt < Diff -> + [{unused, Wt}|apply_diffmap_subtract(Ch, Diff - Wt, T)] + end; +apply_diffmap_subtract(Ch, Diff, [H|T]) -> + [H|apply_diffmap_subtract(Ch, Diff, T)]; +apply_diffmap_subtract(_Ch, _Diff, []) -> + []. + +apply_diffmap_add(Ch, Diff, [{unused, Wt}|T]) -> + if Wt == Diff -> + [{Ch, Wt}|T]; + Wt > Diff -> + [{Ch, Diff}, {unused, Wt - Diff}|T]; + Wt < Diff -> + [{Ch, Wt}|apply_diffmap_add(Ch, Diff - Wt, T)] + end; +apply_diffmap_add(Ch, Diff, [H|T]) -> + [H|apply_diffmap_add(Ch, Diff, T)]; +apply_diffmap_add(_Ch, _Diff, []) -> + []. + +combine_neighbors([{Ch, Wt1}, {Ch, Wt2}|T]) -> + combine_neighbors([{Ch, Wt1 + Wt2}|T]); +combine_neighbors([H|T]) -> + [H|combine_neighbors(T)]; +combine_neighbors([]) -> + []. + +collapse_unused_in_float_map([{Ch, Wt1}, {unused, Wt2}|T]) -> + collapse_unused_in_float_map([{Ch, Wt1 + Wt2}|T]); +collapse_unused_in_float_map([{unused, _}] = L) -> + L; % Degenerate case only +collapse_unused_in_float_map([H|T]) -> + [H|collapse_unused_in_float_map(T)]; +collapse_unused_in_float_map([]) -> + []. + +chash_float_map_to_nextfloat_list(FloatMap) when length(FloatMap) > 0 -> + %% QuickCheck found a bug ... need to weed out stuff smaller than + %% ?SMALLEST_SIGNIFICANT_FLOAT_SIZE here. + FM1 = [P || {_X, Y} = P <- FloatMap, Y > ?SMALLEST_SIGNIFICANT_FLOAT_SIZE], + {_Sum, NFs0} = lists:foldl(fun({Name, Amount}, {Sum, List}) -> + {Sum+Amount, [{Sum+Amount, Name}|List]} + end, {0, []}, FM1), + lists:reverse(NFs0). + +chash_nextfloat_list_to_gb_tree([]) -> + gb_trees:balance(gb_trees:from_orddict([])); +chash_nextfloat_list_to_gb_tree(NextFloatList) -> + {_FloatPos, Name} = lists:last(NextFloatList), + %% QuickCheck found a bug ... it really helps to add a catch-all item + %% at the far "right" of the list ... 42.0 is much greater than 1.0. + NFs = NextFloatList ++ [{42.0, Name}], + gb_trees:balance(gb_trees:from_orddict(orddict:from_list(NFs))). + +-spec chash_gb_next(float(), float_tree()) -> {float(), owner_name()}. +chash_gb_next(X, {_, GbTree}) -> + chash_gb_next1(X, GbTree). + +chash_gb_next1(X, {Key, Val, Left, _Right}) when X < Key -> + case chash_gb_next1(X, Left) of + nil -> + {Key, Val}; + Res -> + Res + end; +chash_gb_next1(X, {Key, _Val, _Left, Right}) when X >= Key -> + chash_gb_next1(X, Right); +chash_gb_next1(_X, nil) -> + nil. + +%% @doc Not used directly, but can give a developer an idea of how well +%% chash_float_map_to_nextfloat_list will do for a given value of Max. +%% +%% For example: +%% +%% NewFloatMap = make_float_map([{unused, 1.0}], +%% [{a,100}, {b, 100}, {c, 10}]), +%% ChashMap = chash_scale_to_int_interval(NewFloatMap, 100), +%% io:format("QQQ: int int = ~p\n", [ChashIntInterval]), +%% -> [{a,1,47},{b,48,94},{c,94,100}] +%% +%% +%% Interpretation: out of the 100 slots: +%%

    +%%
  • 'a' uses the slots 1-47
  • +%%
  • 'b' uses the slots 48-94
  • +%%
  • 'c' uses the slots 95-100
  • +%%
+ +chash_scale_to_int_interval(NewFloatMap, Max) -> + chash_scale_to_int_interval(NewFloatMap, 0, Max). + +%% @type nextfloat_list() = list({float(), brick()}). A nextfloat_list +%% differs from a float_map in two respects: 1) nextfloat_list contains +%% tuples with the brick name in 2nd position, 2) the float() at each +%% position I_n > I_m, for all n, m such that n > m. +%% For example, a nextfloat_list of the float_map example above, +%% [{0.25, {br1, nd1}}, {0.75, {br2, nd1}}, {1.0, {br3, nd1}]. + +chash_scale_to_int_interval([{Ch, _Wt}], Cur, Max) -> + [{Ch, Cur, Max}]; +chash_scale_to_int_interval([{Ch, Wt}|T], Cur, Max) -> + Int = trunc(Wt * Max), + [{Ch, Cur + 1, Cur + Int}|chash_scale_to_int_interval(T, Cur + Int, Max)]. + +%%%%%%%%%%%%% + +%% @doc Make a pretty/human-friendly version of a float map that describes +%% integer ranges between 1 and `Scale'. + +-spec pretty_with_integers(float_map(), integer()) -> [owner_int_range()]. +pretty_with_integers(Map, Scale) -> + chash_scale_to_int_interval(Map, Scale). + +%% @doc Make a pretty/human-friendly version of a float map (based +%% upon a float map created from `OldWeights' and `NewWeights') that +%% describes integer ranges between 1 and `Scale'. + +-spec pretty_with_integers(owner_weight_list(), owner_weight_list(),integer())-> + [owner_int_range()]. +pretty_with_integers(OldWeights, NewWeights, Scale) -> + chash_scale_to_int_interval( + make_float_map(make_float_map(OldWeights), + NewWeights), + Scale). + +%% @doc Create a float tree, which is the rapid lookup data structure +%% for consistent hash queries. + +-spec make_tree(float_map()) -> float_tree(). +make_tree(Map) -> + chash_nextfloat_list_to_gb_tree( + chash_float_map_to_nextfloat_list(Map)). + +%% @doc Low-level function for querying a float tree: the (floating +%% point) point within the unit interval. + +-spec query_tree(float(), float_tree()) -> {float(), owner_name()}. +query_tree(Val, Tree) when is_float(Val), 0.0 =< Val, Val =< 1.0 -> + chash_gb_next(Val, Tree). + +%% @doc Create a sample float map. + +-spec make_demo_map1() -> float_map(). +make_demo_map1() -> + {_, Res} = make_demo_map1_i(), + Res. + +make_demo_map1_i() -> + Fail1 = {b, 100}, + L1 = [{a, 100}, Fail1, {c, 100}], + L2 = L1 ++ [{d, 100}, {e, 100}], + L3 = L2 -- [Fail1], + L4 = L3 ++ [{giant, 300}], + {L4, lists:foldl(fun(New, Old) -> make_float_map(Old, New) end, + make_float_map(L1), [L2, L3, L4])}. + +%% @doc Create a sample float map. + +-spec make_demo_map2() -> float_map(). +make_demo_map2() -> + {L0, _} = make_demo_map1_i(), + L1 = L0 ++ [{h, 100}], + L2 = L1 ++ [{i, 100}], + L3 = L2 ++ [{j, 100}], + lists:foldl(fun(New, Old) -> make_float_map(Old, New) end, + make_demo_map1(), [L1, L2, L3]). + +%% @doc Create a human-friendly summary of a float map. +%% +%% The two parts of the summary are: a per-owner total of the unit +%% interval range(s) owned by each owner, and a total sum of all +%% per-owner ranges (which should be 1.0 but is not enforced). + +-spec sum_map_weights(float_map()) -> + {{per_owner, float_map()}, {weight_sum, float()}}. +sum_map_weights(Map) -> + L = sum_map_weights(lists:sort(Map), undefined, 0.0) -- [{undefined,0.0}], + WeightSum = lists:sum([Weight || {_, Weight} <- L]), + {{per_owner, L}, {weight_sum, WeightSum}}. + +sum_map_weights([{SZ, Weight}|T], SZ, SZ_total) -> + sum_map_weights(T, SZ, SZ_total + Weight); +sum_map_weights([{SZ, Weight}|T], LastSZ, LastSZ_total) -> + [{LastSZ, LastSZ_total}|sum_map_weights(T, SZ, Weight)]; +sum_map_weights([], LastSZ, LastSZ_total) -> + [{LastSZ, LastSZ_total}]. + +%% @doc Query a float map with a binary (inefficient). + +-spec hash_binary_via_float_map(binary(), float_map()) -> + {float(), owner_name()}. +hash_binary_via_float_map(Key, Map) -> + Tree = make_tree(Map), + <> = crypto:hash(sha, Key), + Float = Int / ?SHA_MAX, + query_tree(Float, Tree). + +%% @doc Query a float tree with a binary. + +-spec hash_binary_via_float_tree(binary(), float_tree()) -> + {float(), owner_name()}. +hash_binary_via_float_tree(Key, Tree) -> + <> = crypto:hash(sha, Key), + Float = Int / ?SHA_MAX, + query_tree(Float, Tree). + +%%%%% @doc Various usage examples, see source code below this function +%%%%% for full details. + +zzz_usage_details() -> + +%% %% Make a map. See the code for make_demo_map1() for the order of +%% %% additions & deletions. Here's a brief summary of the 4 steps. +%% %% +%% %% * 'a' through 'e' are weighted @ 100. +%% %% * 'giant' is weighted @ 300. +%% %% * 'b' is removed at step #3. + +%% 40> M1 = machi_chash:make_demo_map1(). +%% [{a,0.09285714285714286}, +%% {giant,0.10714285714285715}, +%% {d,0.026190476190476153}, +%% {giant,0.10714285714285715}, +%% {a,0.04999999999999999}, +%% {giant,0.04999999999999999}, +%% {d,0.04999999999999999}, +%% {giant,0.050000000000000044}, +%% {d,0.06666666666666671}, +%% {e,0.009523809523809434}, +%% {giant,0.05714285714285716}, +%% {c,0.14285714285714285}, +%% {giant,0.05714285714285716}, +%% {e,0.13333333333333341}] + + +%% %% Map M1 onto the interval of integers 0-10,1000 +%% %% +%% %% output = list({SZ_name::term(), Start::integer(), End::integer()}) + +%% 41> machi_chash:pretty_with_integers(M1, 10*1000). +%% [{a,1,928}, +%% {giant,929,1999}, +%% {d,2000,2260}, +%% {giant,2261,3331}, +%% {a,3332,3830}, +%% {giant,3831,4329}, +%% {d,4330,4828}, +%% {giant,4829,5328}, +%% {d,5329,5994}, +%% {e,5995,6089}, +%% {giant,6090,6660}, +%% {c,6661,8088}, +%% {giant,8089,8659}, +%% {e,8659,10000}] + +%% %% Sum up all of the weights, make sure it's what we expect: + +%% 55> machi_chash:sum_map_weights(M1). +%% {{per_owner,[{a,0.14285714285714285}, +%% {c,0.14285714285714285}, +%% {d,0.14285714285714285}, +%% {e,0.14285714285714285}, +%% {giant,0.42857142857142866}]}, +%% {weight_sum,1.0}} + +%% %% Make a tree, then query it +%% %% (Hash::float(), tree()) -> {NextLargestBoundary::float(), szone()} + +%% 58> T1 = machi_chash:make_tree(M1). +%% 59> machi_chash:query_tree(0.2555, T1). +%% {0.3333333333333333,giant} +%% 60> machi_chash:query_tree(0.3555, T1). +%% {0.3833333333333333,a} +%% 61> machi_chash:query_tree(0.4555, T1). +%% {0.4833333333333333,d} + +%% %% How about hashing a bunch of strings and see what happens? + +%% 74> Key1 = "Hello, world!". +%% "Hello, world!" +%% 75> [{K, element(2, machi_chash:hash_binary_via_float_map(K, M1))} || K <- [lists:sublist(Key1, X) || X <- lists:seq(1, length(Key1))]]. +%% [{"H",giant}, +%% {"He",giant}, +%% {"Hel",giant}, +%% {"Hell",e}, +%% {"Hello",e}, +%% {"Hello,",giant}, +%% {"Hello, ",e}, +%% {"Hello, w",e}, +%% {"Hello, wo",giant}, +%% {"Hello, wor",d}, +%% {"Hello, worl",giant}, +%% {"Hello, world",e}, +%% {"Hello, world!",d}] + + ok. diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl new file mode 100644 index 0000000..d78de9d --- /dev/null +++ b/src/machi_flu1.erl @@ -0,0 +1,464 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_flu1). + +-include_lib("kernel/include/file.hrl"). + +-include("machi.hrl"). + +-export([start_link/1, stop/1]). + +start_link([{FluName, TcpPort, DataDir}]) + when is_atom(FluName), is_integer(TcpPort), is_list(DataDir) -> + {ok, spawn_link(fun() -> main2(FluName, TcpPort, DataDir) end)}. + +stop(Pid) -> + case erlang:is_process_alive(Pid) of + true -> + Pid ! forever, + ok; + false -> + error + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +main2(RegName, TcpPort, DataDir) -> + _Pid1 = start_listen_server(RegName, TcpPort, DataDir), + _Pid2 = start_append_server(RegName, DataDir), + receive forever -> ok end. + +start_listen_server(RegName, TcpPort, DataDir) -> + spawn_link(fun() -> run_listen_server(RegName, TcpPort, DataDir) end). + +start_append_server(Name, DataDir) -> + spawn_link(fun() -> run_append_server(Name, DataDir) end). + +run_listen_server(RegName, TcpPort, DataDir) -> + SockOpts = [{reuseaddr, true}, + {mode, binary}, {active, false}, {packet, line}], + {ok, LSock} = gen_tcp:listen(TcpPort, SockOpts), + listen_server_loop(RegName, LSock, DataDir). + +run_append_server(Name, DataDir) -> + register(Name, self()), + append_server_loop(DataDir). + +listen_server_loop(RegName, LSock, DataDir) -> + {ok, Sock} = gen_tcp:accept(LSock), + spawn(fun() -> net_server_loop(RegName, Sock, DataDir) end), + listen_server_loop(RegName, LSock, DataDir). + +append_server_loop(DataDir) -> + receive + {seq_append, From, Prefix, Chunk, CSum} -> + spawn(fun() -> append_server_dispatch(From, Prefix, Chunk, CSum, + DataDir) end), + append_server_loop(DataDir) + end. + +net_server_loop(RegName, Sock, DataDir) -> + ok = inet:setopts(Sock, [{packet, line}]), + case gen_tcp:recv(Sock, 0, 60*1000) of + {ok, Line} -> + %% machi_util:verb("Got: ~p\n", [Line]), + PrefixLenLF = byte_size(Line) - 2 - 8 - 1 - 1, + PrefixLenCRLF = byte_size(Line) - 2 - 8 - 1 - 2, + FileLenLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 1, + FileLenCRLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 2, + CSumFileLenLF = byte_size(Line) - 2 - 1, + CSumFileLenCRLF = byte_size(Line) - 2 - 2, + WriteFileLenLF = byte_size(Line) - 7 - 16 - 1 - 8 - 1 - 1, + DelFileLenLF = byte_size(Line) - 14 - 1, + case Line of + %% For normal use + <<"A ", LenHex:8/binary, " ", + Prefix:PrefixLenLF/binary, "\n">> -> + do_net_server_append(RegName, Sock, LenHex, Prefix); + <<"A ", LenHex:8/binary, " ", + Prefix:PrefixLenCRLF/binary, "\r\n">> -> + do_net_server_append(RegName, Sock, LenHex, Prefix); + <<"R ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", + File:FileLenLF/binary, "\n">> -> + do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); + <<"R ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", + File:FileLenCRLF/binary, "\r\n">> -> + do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); + <<"L\n">> -> + do_net_server_listing(Sock, DataDir); + <<"L\r\n">> -> + do_net_server_listing(Sock, DataDir); + <<"C ", File:CSumFileLenLF/binary, "\n">> -> + do_net_server_checksum_listing(Sock, File, DataDir); + <<"C ", File:CSumFileLenCRLF/binary, "\n">> -> + do_net_server_checksum_listing(Sock, File, DataDir); + <<"QUIT\n">> -> + catch gen_tcp:close(Sock), + exit(normal); + <<"QUIT\r\n">> -> + catch gen_tcp:close(Sock), + exit(normal); + %% For "internal" replication only. + <<"W-repl ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", + File:WriteFileLenLF/binary, "\n">> -> + do_net_server_write(Sock, OffsetHex, LenHex, File, DataDir); + %% For data migration only. + <<"DEL-migration ", File:DelFileLenLF/binary, "\n">> -> + do_net_server_delete_migration_only(Sock, File, DataDir); + %% For erasure coding hackityhack + <<"TRUNC-hack--- ", File:DelFileLenLF/binary, "\n">> -> + do_net_server_truncate_hackityhack(Sock, File, DataDir); + _ -> + machi_util:verb("Else Got: ~p\n", [Line]), + gen_tcp:send(Sock, "ERROR SYNTAX\n"), + catch gen_tcp:close(Sock), + exit(normal) + end, + net_server_loop(RegName, Sock, DataDir); + _ -> + catch gen_tcp:close(Sock), + exit(normal) + end. + +append_server_dispatch(From, Prefix, Chunk, CSum, DataDir) -> + Pid = write_server_get_pid(Prefix, DataDir), + Pid ! {seq_append, From, Prefix, Chunk, CSum}, + exit(normal). + +do_net_server_append(RegName, Sock, LenHex, Prefix) -> + %% TODO: robustify against other invalid path characters such as NUL + case sanitize_file_string(Prefix) of + ok -> + do_net_server_append2(RegName, Sock, LenHex, Prefix); + _ -> + ok = gen_tcp:send(Sock, <<"ERROR BAD-ARG">>) + end. + +sanitize_file_string(Str) -> + case re:run(Str, "/") of + nomatch -> + ok; + _ -> + error + end. + +do_net_server_append2(RegName, Sock, LenHex, Prefix) -> + <> = machi_util:hexstr_to_bin(LenHex), + ok = inet:setopts(Sock, [{packet, raw}]), + {ok, Chunk} = gen_tcp:recv(Sock, Len, 60*1000), + CSum = machi_util:checksum(Chunk), + try + RegName ! {seq_append, self(), Prefix, Chunk, CSum} + catch error:badarg -> + error_logger:error_msg("Message send to ~p gave badarg, make certain server is running with correct registered name\n", [?MODULE]) + end, + receive + {assignment, Offset, File} -> + OffsetHex = machi_util:bin_to_hexstr(<>), + Out = io_lib:format("OK ~s ~s\n", [OffsetHex, File]), + ok = gen_tcp:send(Sock, Out) + after 10*1000 -> + ok = gen_tcp:send(Sock, "TIMEOUT\n") + end. + +do_net_server_read(Sock, OffsetHex, LenHex, FileBin, DataDir) -> + DoItFun = fun(FH, Offset, Len) -> + case file:pread(FH, Offset, Len) of + {ok, Bytes} when byte_size(Bytes) == Len -> + gen_tcp:send(Sock, ["OK\n", Bytes]); + {ok, Bytes} -> + machi_util:verb("ok read but wanted ~p got ~p: ~p @ offset ~p\n", + [Len, size(Bytes), FileBin, Offset]), + ok = gen_tcp:send(Sock, "ERROR PARTIAL-READ\n"); + eof -> + perhaps_do_net_server_ec_read(Sock, FH); + _Else2 -> + machi_util:verb("Else2 ~p ~p ~P\n", + [Offset, Len, _Else2, 20]), + ok = gen_tcp:send(Sock, "ERROR BAD-READ\n") + end + end, + do_net_server_readwrite_common(Sock, OffsetHex, LenHex, FileBin, DataDir, + [read, binary, raw], DoItFun). + +do_net_server_readwrite_common(Sock, OffsetHex, LenHex, FileBin, DataDir, + FileOpts, DoItFun) -> + case sanitize_file_string(FileBin) of + ok -> + do_net_server_readwrite_common2(Sock, OffsetHex, LenHex, FileBin, + DataDir, FileOpts, DoItFun); + _ -> + ok = gen_tcp:send(Sock, <<"ERROR BAD-ARG\n">>) + end. + +do_net_server_readwrite_common2(Sock, OffsetHex, LenHex, FileBin, DataDir, + FileOpts, DoItFun) -> + <> = machi_util:hexstr_to_bin(OffsetHex), + <> = machi_util:hexstr_to_bin(LenHex), + {_, Path} = machi_util:make_data_filename(DataDir, FileBin), + OptsHasWrite = lists:member(write, FileOpts), + case file:open(Path, FileOpts) of + {ok, FH} -> + try + DoItFun(FH, Offset, Len) + after + file:close(FH) + end; + {error, enoent} when OptsHasWrite -> + ok = filelib:ensure_dir(Path), + do_net_server_readwrite_common( + Sock, OffsetHex, LenHex, FileBin, DataDir, + FileOpts, DoItFun); + _Else -> + %%%%%% keep?? machi_util:verb("Else ~p ~p ~p ~p\n", [Offset, Len, Path, _Else]), + ok = gen_tcp:send(Sock, <<"ERROR BAD-IO\n">>) + end. + + +do_net_server_write(Sock, OffsetHex, LenHex, FileBin, DataDir) -> + CSumPath = machi_util:make_checksum_filename(DataDir, FileBin), + case file:open(CSumPath, [append, raw, binary, delayed_write]) of + {ok, FHc} -> + do_net_server_write2(Sock, OffsetHex, LenHex, FileBin, DataDir, FHc); + {error, enoent} -> + ok = filelib:ensure_dir(CSumPath), + do_net_server_write(Sock, OffsetHex, LenHex, FileBin, DataDir) + end. + +do_net_server_write2(Sock, OffsetHex, LenHex, FileBin, DataDir, FHc) -> + DoItFun = fun(FHd, Offset, Len) -> + ok = inet:setopts(Sock, [{packet, raw}]), + {ok, Chunk} = gen_tcp:recv(Sock, Len), + CSum = machi_util:checksum(Chunk), + case file:pwrite(FHd, Offset, Chunk) of + ok -> + CSumHex = machi_util:bin_to_hexstr(CSum), + CSum_info = [OffsetHex, 32, LenHex, 32, CSumHex, 10], + ok = file:write(FHc, CSum_info), + ok = file:close(FHc), + gen_tcp:send(Sock, <<"OK\n">>); + _Else3 -> + machi_util:verb("Else3 ~p ~p ~p\n", + [Offset, Len, _Else3]), + ok = gen_tcp:send(Sock, "ERROR BAD-PWRITE\n") + end + end, + do_net_server_readwrite_common(Sock, OffsetHex, LenHex, FileBin, DataDir, + [write, read, binary, raw], DoItFun). + +perhaps_do_net_server_ec_read(Sock, FH) -> + case file:pread(FH, 0, ?MINIMUM_OFFSET) of + {ok, Bin} when byte_size(Bin) == ?MINIMUM_OFFSET -> + decode_and_reply_net_server_ec_read(Sock, Bin); + {ok, _AnythingElse} -> + ok = gen_tcp:send(Sock, "ERROR PARTIAL-READ2\n"); + _AnythingElse -> + ok = gen_tcp:send(Sock, "ERROR BAD-PREAD\n") + end. + +decode_and_reply_net_server_ec_read(Sock, <<"a ", Rest/binary>>) -> + decode_and_reply_net_server_ec_read_version_a(Sock, Rest); +decode_and_reply_net_server_ec_read(Sock, <<0:8, _/binary>>) -> + ok = gen_tcp:send(Sock, <<"ERROR NOT-ERASURE\n">>). + +decode_and_reply_net_server_ec_read_version_a(Sock, Rest) -> + %% <> = Rest, + HdrLen = 80 - 2 - 4 - 1, + <> = Rest, + <> = machi_util:hexstr_to_bin(BodyLenHex), + <> = Rest2, + ok = gen_tcp:send(Sock, ["ERASURE ", BodyLenHex, " ", Hdr, Body]). + +do_net_server_listing(Sock, DataDir) -> + Files = filelib:wildcard("*", DataDir) -- ["config"], + Out = ["OK\n", + [begin + {ok, FI} = file:read_file_info(DataDir ++ "/" ++ File), + Size = FI#file_info.size, + SizeBin = <>, + [machi_util:bin_to_hexstr(SizeBin), <<" ">>, + list_to_binary(File), <<"\n">>] + end || File <- Files], + ".\n" + ], + ok = gen_tcp:send(Sock, Out). + +do_net_server_checksum_listing(Sock, File, DataDir) -> + case sanitize_file_string(File) of + ok -> + do_net_server_checksum_listing2(Sock, File, DataDir); + _ -> + ok = gen_tcp:send(Sock, <<"ERROR BAD-ARG\n">>) + end. + +do_net_server_checksum_listing2(Sock, File, DataDir) -> + CSumPath = machi_util:make_checksum_filename(DataDir, File), + case file:open(CSumPath, [read, raw, binary]) of + {ok, FH} -> + {ok, FI} = file:read_file_info(CSumPath), + Len = FI#file_info.size, + LenHex = list_to_binary(machi_util:bin_to_hexstr(<>)), + %% Client has option of line-by-line with "." terminator, + %% or using the offset in the OK message to slurp things + %% down by exact byte size. + ok = gen_tcp:send(Sock, [<<"OK ">>, LenHex, <<"\n">>]), + do_net_copy_bytes(FH, Sock), + ok = file:close(FH), + ok = gen_tcp:send(Sock, ".\n"); + {error, enoent} -> + ok = gen_tcp:send(Sock, "ERROR NO-SUCH-FILE\n"); + _ -> + ok = gen_tcp:send(Sock, "ERROR\n") + end. + +do_net_copy_bytes(FH, Sock) -> + case file:read(FH, 1024*1024) of + {ok, Bin} -> + ok = gen_tcp:send(Sock, Bin), + do_net_copy_bytes(FH, Sock); + eof -> + ok + end. + +do_net_server_delete_migration_only(Sock, File, DataDir) -> + case sanitize_file_string(File) of + ok -> + do_net_server_delete_migration_only2(Sock, File, DataDir); + _ -> + ok = gen_tcp:send(Sock, <<"ERROR BAD-ARG\n">>) + end. + +do_net_server_delete_migration_only2(Sock, File, DataDir) -> + {_, Path} = machi_util:make_data_filename(DataDir, File), + case file:delete(Path) of + ok -> + ok = gen_tcp:send(Sock, "OK\n"); + {error, enoent} -> + ok = gen_tcp:send(Sock, "ERROR NO-SUCH-FILE\n"); + _ -> + ok = gen_tcp:send(Sock, "ERROR\n") + end. + +do_net_server_truncate_hackityhack(Sock, File, DataDir) -> + case sanitize_file_string(File) of + ok -> + do_net_server_truncate_hackityhack2(Sock, File, DataDir); + _ -> + ok = gen_tcp:send(Sock, <<"ERROR BAD-ARG\n">>) + end. + +do_net_server_truncate_hackityhack2(Sock, File, DataDir) -> + {_, Path} = machi_util:make_data_filename(DataDir, File), + case file:open(Path, [read, write, binary, raw]) of + {ok, FH} -> + try + {ok, ?MINIMUM_OFFSET} = file:position(FH, ?MINIMUM_OFFSET), + ok = file:truncate(FH), + ok = gen_tcp:send(Sock, "OK\n") + after + file:close(FH) + end; + {error, enoent} -> + ok = gen_tcp:send(Sock, "ERROR NO-SUCH-FILE\n"); + _ -> + ok = gen_tcp:send(Sock, "ERROR\n") + end. + +write_server_get_pid(Prefix, DataDir) -> + RegName = machi_util:make_regname(Prefix), + case whereis(RegName) of + undefined -> + start_seq_append_server(Prefix, DataDir), + timer:sleep(1), + write_server_get_pid(Prefix, DataDir); + Pid -> + Pid + end. + +start_seq_append_server(Prefix, DataDir) -> + spawn_link(fun() -> run_seq_append_server(Prefix, DataDir) end). + +run_seq_append_server(Prefix, DataDir) -> + true = register(machi_util:make_regname(Prefix), self()), + ok = filelib:ensure_dir(DataDir ++ "/unused"), + ok = filelib:ensure_dir(DataDir ++ "/config/unused"), + run_seq_append_server2(Prefix, DataDir). + +run_seq_append_server2(Prefix, DataDir) -> + FileNum = machi_util:read_max_filenum(DataDir, Prefix) + 1, + case machi_util:increment_max_filenum(DataDir, Prefix) of + ok -> + machi_util:increment_max_filenum(DataDir, Prefix), + machi_util:info_msg("start: ~p server at file ~w\n", + [Prefix, FileNum]), + seq_append_server_loop(DataDir, Prefix, FileNum); + Else -> + error_logger:error_msg("start: ~p server at file ~w: ~p\n", + [Prefix, FileNum, Else]), + exit(Else) + + end. + +seq_append_server_loop(DataDir, Prefix, FileNum) -> + SequencerNameHack = lists:flatten(io_lib:format( + "~.36B~.36B", + [element(3,now()), + list_to_integer(os:getpid())])), + {File, FullPath} = machi_util:make_data_filename( + DataDir, Prefix, SequencerNameHack, FileNum), + {ok, FHd} = file:open(FullPath, + [write, binary, raw]), + %% [write, binary, raw, delayed_write]), + CSumPath = machi_util:make_checksum_filename( + DataDir, Prefix, SequencerNameHack, FileNum), + {ok, FHc} = file:open(CSumPath, [append, raw, binary, delayed_write]), + seq_append_server_loop(DataDir, Prefix, File, {FHd,FHc}, FileNum, + ?MINIMUM_OFFSET). + +seq_append_server_loop(DataDir, Prefix, _File, {FHd,FHc}, FileNum, Offset) + when Offset > ?MAX_FILE_SIZE -> + ok = file:close(FHd), + ok = file:close(FHc), + machi_util:info_msg("rollover: ~p server at file ~w offset ~w\n", + [Prefix, FileNum, Offset]), + run_seq_append_server2(Prefix, DataDir); +seq_append_server_loop(DataDir, Prefix, File, {FHd,FHc}=FH_, FileNum, Offset) -> + receive + {seq_append, From, Prefix, Chunk, CSum} -> + ok = file:pwrite(FHd, Offset, Chunk), + From ! {assignment, Offset, File}, + Len = byte_size(Chunk), + OffsetHex = machi_util:bin_to_hexstr(<>), + LenHex = machi_util:bin_to_hexstr(<>), + CSumHex = machi_util:bin_to_hexstr(CSum), + CSum_info = [OffsetHex, 32, LenHex, 32, CSumHex, 10], + ok = file:write(FHc, CSum_info), + seq_append_server_loop(DataDir, Prefix, File, FH_, + FileNum, Offset + Len) + after 30*1000 -> + ok = file:close(FHd), + ok = file:close(FHc), + machi_util:info_msg("stop: ~p server at file ~w offset ~w\n", + [Prefix, FileNum, Offset]), + exit(normal) + end. + diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl new file mode 100644 index 0000000..3cccf00 --- /dev/null +++ b/src/machi_flu1_client.erl @@ -0,0 +1,399 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_flu1_client). + +-include("machi.hrl"). + +-export([ + append_chunk/3, append_chunk/4, + read_chunk/4, read_chunk/5, + checksum_list/2, checksum_list/3, + list_files/1, list_files/2, + quit/1 + ]). +%% For "internal" replication only. +-export([ + write_chunk/4, write_chunk/5, + delete_migration/2, delete_migration/3, + trunc_hack/2, trunc_hack/3 + ]). + +-type chunk() :: iolist(). +-type chunk_s() :: binary(). +-type chunk_pos() :: {file_offset(), chunk_size(), file_name_s()}. +-type chunk_size() :: non_neg_integer(). +-type inet_host() :: inet:ip_address() | inet:hostname(). +-type inet_port() :: inet:port_number(). +-type file_name() :: binary() | list(). +-type file_name_s() :: binary(). % server reply +-type file_offset() :: non_neg_integer(). +-type file_prefix() :: binary() | list(). + +%% @doc Append a chunk (binary- or iolist-style) of data to a file +%% with `Prefix'. + +-spec append_chunk(port(), file_prefix(), chunk()) -> + {ok, chunk_pos()} | {error, term()}. +append_chunk(Sock, Prefix, Chunk) -> + append_chunk2(Sock, Prefix, Chunk). + +%% @doc Append a chunk (binary- or iolist-style) of data to a file +%% with `Prefix'. + +-spec append_chunk(inet_host(), inet_port(), file_prefix(), chunk()) -> + {ok, chunk_pos()} | {error, term()}. +append_chunk(Host, TcpPort, Prefix, Chunk) -> + Sock = machi_util:connect(Host, TcpPort), + try + append_chunk2(Sock, Prefix, Chunk) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Read a chunk of data of size `Size' from `File' at `Offset'. + +-spec read_chunk(port(), file_name(), file_offset(), chunk_size()) -> + {ok, chunk_s()} | {error, term()}. +read_chunk(Sock, File, Offset, Size) -> + read_chunk2(Sock, File, Offset, Size). + +%% @doc Read a chunk of data of size `Size' from `File' at `Offset'. + +-spec read_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk_size()) -> + {ok, chunk_s()} | {error, term()}. +read_chunk(Host, TcpPort, File, Offset, Size) -> + Sock = machi_util:connect(Host, TcpPort), + try + read_chunk2(Sock, File, Offset, Size) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Fetch the list of chunk checksums for `File'. + +-spec checksum_list(port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +checksum_list(Sock, File) when is_port(Sock) -> + checksum_list2(Sock, File). + +%% @doc Fetch the list of chunk checksums for `File'. + +-spec checksum_list(inet_host(), inet_port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +checksum_list(Host, TcpPort, File) when is_integer(TcpPort) -> + Sock = machi_util:connect(Host, TcpPort), + try + checksum_list2(Sock, File) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Fetch the list of all files on the remote FLU. + +-spec list_files(port()) -> + {ok, [file_name()]} | {error, term()}. +list_files(Sock) when is_port(Sock) -> + list2(Sock). + +%% @doc Fetch the list of all files on the remote FLU. + +-spec list_files(inet_host(), inet_port()) -> + {ok, [file_name()]} | {error, term()}. +list_files(Host, TcpPort) when is_integer(TcpPort) -> + Sock = machi_util:connect(Host, TcpPort), + try + list2(Sock) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Quit & close the connection to remote FLU. + +-spec quit(port()) -> + ok. +quit(Sock) when is_port(Sock) -> + catch (_ = gen_tcp:send(Sock, <<"QUIT\n">>)), + catch gen_tcp:close(Sock), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% @doc Restricted API: Write a chunk of already-sequenced data to +%% `File' at `Offset'. + +-spec write_chunk(port(), file_name(), file_offset(), chunk()) -> + {ok, chunk_s()} | {error, term()}. +write_chunk(Sock, File, Offset, Chunk) -> + write_chunk2(Sock, File, Offset, Chunk). + +%% @doc Restricted API: Write a chunk of already-sequenced data to +%% `File' at `Offset'. + +-spec write_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk()) -> + {ok, chunk_s()} | {error, term()}. +write_chunk(Host, TcpPort, File, Offset, Chunk) -> + Sock = machi_util:connect(Host, TcpPort), + try + write_chunk2(Sock, File, Offset, Chunk) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Restricted API: Delete a file after it has been successfully +%% migrated. + +-spec delete_migration(port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +delete_migration(Sock, File) when is_port(Sock) -> + delete_migration2(Sock, File). + +%% @doc Restricted API: Delete a file after it has been successfully +%% migrated. + +-spec delete_migration(inet_host(), inet_port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +delete_migration(Host, TcpPort, File) when is_integer(TcpPort) -> + Sock = machi_util:connect(Host, TcpPort), + try + delete_migration2(Sock, File) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Restricted API: Truncate a file after it has been successfully +%% erasure coded. + +-spec trunc_hack(port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +trunc_hack(Sock, File) when is_port(Sock) -> + trunc_hack2(Sock, File). + +%% @doc Restricted API: Truncate a file after it has been successfully +%% erasure coded. + +-spec trunc_hack(inet_host(), inet_port(), file_name()) -> + {ok, [file_name()]} | {error, term()}. +trunc_hack(Host, TcpPort, File) when is_integer(TcpPort) -> + Sock = machi_util:connect(Host, TcpPort), + try + trunc_hack2(Sock, File) + after + catch gen_tcp:close(Sock) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +append_chunk2(Sock, Prefix0, Chunk0) -> + try + %% TODO: add client-side checksum to the server's protocol + %% _ = crypto:hash(md5, Chunk), + Prefix = machi_util:make_binary(Prefix0), + Chunk = machi_util:make_binary(Chunk0), + Len = iolist_size(Chunk0), + true = (Len =< ?MAX_CHUNK_SIZE), + LenHex = machi_util:int_to_hexbin(Len, 32), + Cmd = <<"A ", LenHex/binary, " ", Prefix/binary, "\n">>, + ok = gen_tcp:send(Sock, [Cmd, Chunk]), + {ok, Line} = gen_tcp:recv(Sock, 0), + PathLen = byte_size(Line) - 3 - 16 - 1 - 1, + case Line of + <<"OK ", OffsetHex:16/binary, " ", + Path:PathLen/binary, _:1/binary>> -> + Offset = machi_util:hexstr_to_int(OffsetHex), + {ok, {Offset, Len, Path}}; + <<"ERROR BAD-ARG", _/binary>> -> + {error, bad_arg}; + <<"ERROR ", Rest/binary>> -> + {error, Rest} + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch, erlang:get_stacktrace()}} + end. + +read_chunk2(Sock, File0, Offset, Size) -> + File = machi_util:make_binary(File0), + PrefixHex = machi_util:int_to_hexbin(Offset, 64), + SizeHex = machi_util:int_to_hexbin(Size, 32), + CmdLF = [$R, 32, PrefixHex, 32, SizeHex, 32, File, 10], + ok = gen_tcp:send(Sock, CmdLF), + case gen_tcp:recv(Sock, 3) of + {ok, <<"OK\n">>} -> + {ok, _Chunk}=Res = gen_tcp:recv(Sock, Size), + Res; + {ok, Else} -> + {ok, OldOpts} = inet:getopts(Sock, [packet]), + ok = inet:setopts(Sock, [{packet, line}]), + {ok, Else2} = gen_tcp:recv(Sock, 0), + ok = inet:setopts(Sock, OldOpts), + case Else of + <<"ERA">> -> + {error, todo_erasure_coded}; %% escript_cc_parse_ec_info(Sock, Line, Else2); + <<"ERR">> -> + case Else2 of + <<"OR BAD-IO\n">> -> + {error, no_such_file}; + <<"OR NOT-ERASURE\n">> -> + {error, no_such_file}; + <<"OR BAD-ARG\n">> -> + {error, bad_arg}; + <<"OR PARTIAL-READ\n">> -> + {error, partial_read}; + _ -> + {error, Else2} + end; + _ -> + {error, {whaaa, <>}} + end + end. + +list2(Sock) -> + try + ok = gen_tcp:send(Sock, <<"L\n">>), + ok = inet:setopts(Sock, [{packet, line}]), + {ok, <<"OK\n">>} = gen_tcp:recv(Sock, 0), + Res = list2(gen_tcp:recv(Sock, 0), Sock), + ok = inet:setopts(Sock, [{packet, raw}]), + {ok, Res} + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch}} + end. + +list2({ok, <<".\n">>}, _Sock) -> + []; +list2({ok, Line}, Sock) -> + [Line|list2(gen_tcp:recv(Sock, 0), Sock)]; +list2(Else, _Sock) -> + throw({server_protocol_error, Else}). + +checksum_list2(Sock, File) -> + try + ok = gen_tcp:send(Sock, [<<"C ">>, File, <<"\n">>]), + ok = inet:setopts(Sock, [{packet, line}]), + case gen_tcp:recv(Sock, 0) of + {ok, <<"OK ", Rest/binary>> = Line} -> + put(status, ok), % may be unset later + RestLen = byte_size(Rest) - 1, + <> = Rest, + <> = machi_util:hexstr_to_bin(LenHex), + ok = inet:setopts(Sock, [{packet, raw}]), + checksum_list_fast(Sock, Len); + {ok, <<"ERROR NO-SUCH-FILE", _/binary>>} -> + {error, no_such_file}; + {ok, <<"ERROR BAD-ARG", _/binary>>} -> + {error, bad_arg}; + {ok, Else} -> + throw({server_protocol_error, Else}) + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch}} + end. + +checksum_list_fast(Sock, 0) -> + {ok, <<".\n">> = _Line} = gen_tcp:recv(Sock, 2), + []; +checksum_list_fast(Sock, Remaining) -> + Num = erlang:min(Remaining, 1024*1024), + {ok, Bytes} = gen_tcp:recv(Sock, Num), + [Bytes|checksum_list_fast(Sock, Remaining - byte_size(Bytes))]. + + +write_chunk2(Sock, File0, Offset, Chunk0) -> + try + %% TODO: add client-side checksum to the server's protocol + %% _ = crypto:hash(md5, Chunk), + File = machi_util:make_binary(File0), + true = (Offset >= ?MINIMUM_OFFSET), + OffsetHex = machi_util:int_to_hexbin(Offset, 64), + Chunk = machi_util:make_binary(Chunk0), + Len = iolist_size(Chunk0), + true = (Len =< ?MAX_CHUNK_SIZE), + LenHex = machi_util:int_to_hexbin(Len, 32), + Cmd = <<"W-repl ", OffsetHex/binary, " ", + LenHex/binary, " ", File/binary, "\n">>, + + ok = gen_tcp:send(Sock, [Cmd, Chunk]), + {ok, Line} = gen_tcp:recv(Sock, 0), + PathLen = byte_size(Line) - 3 - 16 - 1 - 1, + case Line of + <<"OK\n">> -> + ok; + <<"ERROR BAD-ARG", _/binary>> -> + {error, bad_arg}; + <<"ERROR ", _/binary>>=Else -> + {error, {server_said, Else}} + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch, erlang:get_stacktrace()}} + end. + +delete_migration2(Sock, File) -> + try + ok = gen_tcp:send(Sock, [<<"DEL-migration ">>, File, <<"\n">>]), + ok = inet:setopts(Sock, [{packet, line}]), + case gen_tcp:recv(Sock, 0) of + {ok, <<"OK\n">>} -> + ok; + {ok, <<"ERROR NO-SUCH-FILE", _/binary>>} -> + {error, no_such_file}; + {ok, <<"ERROR BAD-ARG", _/binary>>} -> + {error, bad_arg}; + {ok, Else} -> + throw({server_protocol_error, Else}) + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch}} + end. + +trunc_hack2(Sock, File) -> + try + ok = gen_tcp:send(Sock, [<<"TRUNC-hack--- ">>, File, <<"\n">>]), + ok = inet:setopts(Sock, [{packet, line}]), + case gen_tcp:recv(Sock, 0) of + {ok, <<"OK\n">>} -> + ok; + {ok, <<"ERROR NO-SUCH-FILE", _/binary>>} -> + {error, no_such_file}; + {ok, <<"ERROR BAD-ARG", _/binary>>} -> + {error, bad_arg}; + {ok, Else} -> + throw({server_protocol_error, Else}) + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch}} + end. diff --git a/src/machi_flu_sup.erl b/src/machi_flu_sup.erl new file mode 100644 index 0000000..4ad26fc --- /dev/null +++ b/src/machi_flu_sup.erl @@ -0,0 +1,51 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_flu_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + RestartStrategy = one_for_one, + MaxRestarts = 1000, + MaxSecondsBetweenRestarts = 3600, + + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + + Restart = permanent, + Shutdown = 5000, + Type = worker, + + {ok, FluList} = application:get_env(machi, flu_list), + FluSpecs = [{FluName, {machi_flu, start_link, [FluArgs]}, + Restart, Shutdown, Type, []} || + {FluName, _Port, _Dir}=FluArgs <- FluList], + {ok, {SupFlags, FluSpecs}}. diff --git a/src/machi_sequencer.erl b/src/machi_sequencer.erl new file mode 100644 index 0000000..ddd81a5 --- /dev/null +++ b/src/machi_sequencer.erl @@ -0,0 +1,191 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_sequencer). + +-compile(export_all). + +-include_lib("kernel/include/file.hrl"). + +-define(CONFIG_DIR, "./config"). +-define(DATA_DIR, "./data"). + +seq(Server, Prefix, Size) when is_binary(Prefix), is_integer(Size), Size > -1 -> + Server ! {seq, self(), Prefix, Size}, + receive + {assignment, File, Offset} -> + {File, Offset} + after 1*1000 -> + bummer + end. + +seq_direct(Prefix, Size) when is_binary(Prefix), is_integer(Size), Size > -1 -> + RegName = make_regname(Prefix), + seq(RegName, Prefix, Size). + +start_server() -> + start_server(?MODULE). + +start_server(Name) -> + spawn_link(fun() -> run_server(Name) end). + +run_server(Name) -> + register(Name, self()), + ets:new(?MODULE, [named_table, public, {write_concurrency, true}]), + server_loop(). + +server_loop() -> + receive + {seq, From, Prefix, Size} -> + spawn(fun() -> server_dispatch(From, Prefix, Size) end), + server_loop() + end. + +server_dispatch(From, Prefix, Size) -> + RegName = make_regname(Prefix), + case whereis(RegName) of + undefined -> + start_prefix_server(Prefix), + timer:sleep(1), + server_dispatch(From, Prefix, Size); + Pid -> + Pid ! {seq, From, Prefix, Size} + end, + exit(normal). + +start_prefix_server(Prefix) -> + spawn(fun() -> run_prefix_server(Prefix) end). + +run_prefix_server(Prefix) -> + true = register(make_regname(Prefix), self()), + ok = filelib:ensure_dir(?CONFIG_DIR ++ "/unused"), + ok = filelib:ensure_dir(?DATA_DIR ++ "/unused"), + FileNum = read_max_filenum(Prefix) + 1, + ok = increment_max_filenum(Prefix), + prefix_server_loop(Prefix, FileNum). + +prefix_server_loop(Prefix, FileNum) -> + File = make_data_filename(Prefix, FileNum), + prefix_server_loop(Prefix, File, FileNum, 0). + +prefix_server_loop(Prefix, File, FileNum, Offset) -> + receive + {seq, From, Prefix, Size} -> + From ! {assignment, File, Offset}, + prefix_server_loop(Prefix, File, FileNum, Offset + Size) + after 30*1000 -> + io:format("timeout: ~p server stopping\n", [Prefix]), + exit(normal) + end. + +make_regname(Prefix) -> + erlang:binary_to_atom(Prefix, latin1). + +make_config_filename(Prefix) -> + lists:flatten(io_lib:format("~s/~s", [?CONFIG_DIR, Prefix])). + +make_data_filename(Prefix, FileNum) -> + erlang:iolist_to_binary(io_lib:format("~s/~s.~w", + [?DATA_DIR, Prefix, FileNum])). + +read_max_filenum(Prefix) -> + case file:read_file_info(make_config_filename(Prefix)) of + {error, enoent} -> + 0; + {ok, FI} -> + FI#file_info.size + end. + +increment_max_filenum(Prefix) -> + {ok, FH} = file:open(make_config_filename(Prefix), [append]), + ok = file:write(FH, "x"), + %% ok = file:sync(FH), + ok = file:close(FH). + +%%%%%%%%%%%%%%%%% + +%% basho_bench callbacks + +-define(SEQ, ?MODULE). + +new(1) -> + start_server(), + timer:sleep(100), + {ok, unused}; +new(_Id) -> + {ok, unused}. + +run(null, _KeyGen, _ValgueGen, State) -> + {ok, State}; +run(keygen_then_null, KeyGen, _ValgueGen, State) -> + _Prefix = KeyGen(), + {ok, State}; +run(seq, KeyGen, _ValgueGen, State) -> + Prefix = KeyGen(), + {_, _} = ?SEQ:seq(?SEQ, Prefix, 1), + {ok, State}; +run(seq_direct, KeyGen, _ValgueGen, State) -> + Prefix = KeyGen(), + Name = ?SEQ:make_regname(Prefix), + case get(Name) of + undefined -> + case whereis(Name) of + undefined -> + {_, _} = ?SEQ:seq(?SEQ, Prefix, 1); + Pid -> + put(Name, Pid), + {_, _} = ?SEQ:seq(Pid, Prefix, 1) + end; + Pid -> + {_, _} = ?SEQ:seq(Pid, Prefix, 1) + end, + {ok, State}; +run(seq_ets, KeyGen, _ValgueGen, State) -> + Tab = ?MODULE, + Prefix = KeyGen(), + Res = try + BigNum = ets:update_counter(Tab, Prefix, 1), + BigBin = <>, + <> = BigBin, + %% if Offset rem 1000 == 0 -> + %% io:format("~p,~p ", [FileNum, Offset]); + %% true -> + %% ok + %% end, + {fakefake, FileNum, Offset} + catch error:badarg -> + FileNum2 = 1, Offset2 = 0, + FileBin = <>, + OffsetBin = <>, + Glop = <>, + <> = Glop, + %% if Prefix == <<"42">> -> io:format("base:~w\n", [Base]); true -> ok end, + %% Base = 0, + case ets:insert_new(Tab, {Prefix, Base}) of + true -> + {<<"fakefakefake">>, Base}; + false -> + Result2 = ets:update_counter(Tab, Prefix, 1), + {<<"fakefakefake">>, Result2} + end + end, + Res = Res, + {ok, State}. + diff --git a/src/machi_sup.erl b/src/machi_sup.erl new file mode 100644 index 0000000..dcaadbe --- /dev/null +++ b/src/machi_sup.erl @@ -0,0 +1,55 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +init([]) -> + RestartStrategy = one_for_one, + MaxRestarts = 1000, + MaxSecondsBetweenRestarts = 3600, + + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + + Restart = permanent, + Shutdown = 5000, + Type = supervisor, + + ServerSup = + {machi_flu_sup, {machi_flu_sup, start_link, []}, + Restart, Shutdown, Type, []}, + + {ok, {SupFlags, [ServerSup]}}. + + %% AChild = {'AName', {'AModule', start_link, []}, + %% Restart, Shutdown, Type, ['AModule']}, + %% {ok, {SupFlags, [AChild]}}. diff --git a/src/machi_util.erl b/src/machi_util.erl new file mode 100644 index 0000000..c859574 --- /dev/null +++ b/src/machi_util.erl @@ -0,0 +1,1102 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_util). + +-export([ + checksum/1, + hexstr_to_bin/1, bin_to_hexstr/1, + hexstr_to_int/1, int_to_hexstr/2, int_to_hexbin/2, + make_binary/1, + make_regname/1, + make_checksum_filename/2, make_data_filename/2, + read_max_filenum/2, increment_max_filenum/2, + info_msg/2, verb/1, verb/2, + %% TCP protocol helpers + connect/2 + ]). +-compile(export_all). + +-include("machi.hrl"). +-include("machi_projection.hrl"). +-include_lib("kernel/include/file.hrl"). + +append(Server, Prefix, Chunk) when is_binary(Prefix), is_binary(Chunk) -> + CSum = checksum(Chunk), + Server ! {seq_append, self(), Prefix, Chunk, CSum}, + receive + {assignment, Offset, File} -> + {Offset, File} + after 10*1000 -> + bummer + end. + +make_regname(Prefix) when is_binary(Prefix) -> + erlang:binary_to_atom(Prefix, latin1); +make_regname(Prefix) when is_list(Prefix) -> + erlang:list_to_atom(Prefix). + +make_config_filename(DataDir, Prefix) -> + lists:flatten(io_lib:format("~s/config/~s", [DataDir, Prefix])). + +make_checksum_filename(DataDir, Prefix, SequencerName, FileNum) -> + lists:flatten(io_lib:format("~s/config/~s.~s.~w.csum", + [DataDir, Prefix, SequencerName, FileNum])). + +make_checksum_filename(DataDir, FileName) -> + lists:flatten(io_lib:format("~s/config/~s.csum", [DataDir, FileName])). + +make_data_filename(DataDir, File) -> + FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])), + {File, FullPath}. + +make_data_filename(DataDir, Prefix, SequencerName, FileNum) -> + File = erlang:iolist_to_binary(io_lib:format("~s.~s.~w", + [Prefix, SequencerName, FileNum])), + FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])), + {File, FullPath}. + +read_max_filenum(DataDir, Prefix) -> + case file:read_file_info(make_config_filename(DataDir, Prefix)) of + {error, enoent} -> + 0; + {ok, FI} -> + FI#file_info.size + end. + +increment_max_filenum(DataDir, Prefix) -> + try + {ok, FH} = file:open(make_config_filename(DataDir, Prefix), [append]), + ok = file:write(FH, "x"), + %% ok = file:sync(FH), + ok = file:close(FH) + catch + error:{badmatch,_}=Error -> + {error, Error, erlang:get_stacktrace()} + end. + +hexstr_to_bin(S) when is_list(S) -> + hexstr_to_bin(S, []); +hexstr_to_bin(B) when is_binary(B) -> + hexstr_to_bin(binary_to_list(B), []). + +hexstr_to_bin([], Acc) -> + list_to_binary(lists:reverse(Acc)); +hexstr_to_bin([X,Y|T], Acc) -> + {ok, [V], []} = io_lib:fread("~16u", [X,Y]), + hexstr_to_bin(T, [V | Acc]). + +bin_to_hexstr(<<>>) -> + []; +bin_to_hexstr(<>) -> + [hex_digit(X), hex_digit(Y)|bin_to_hexstr(Rest)]. + +hex_digit(X) when X < 10 -> + X + $0; +hex_digit(X) -> + X - 10 + $a. + +make_binary(X) when is_binary(X) -> + X; +make_binary(X) when is_list(X) -> + iolist_to_binary(X). + +hexstr_to_int(X) -> + B = hexstr_to_bin(X), + B_size = byte_size(B) * 8, + <> = B, + I. + +int_to_hexstr(I, I_size) -> + bin_to_hexstr(<>). + +int_to_hexbin(I, I_size) -> + list_to_binary(int_to_hexstr(I, I_size)). + +%%%%%%%%%%%%%%%%% + +%%% escript stuff + +main2(["1file-write-redundant-client"]) -> + io:format("Use: Write a local file to a series of servers.\n"), + io:format("Args: BlockSize Prefix LocalFilePath [silent] [Host Port [Host Port ...]]\n"), + erlang:halt(1); +main2(["1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile|HPs0]) -> + BlockSize = list_to_integer(BlockSizeStr), + Prefix = list_to_binary(PrefixStr), + {Out, HPs} = case HPs0 of + ["silent"|Rest] -> {silent, Rest}; + _ -> {not_silent, HPs0} + end, + Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile), + if Out /= silent -> + print_upload_details(user, Res); + true -> + ok + end, + Res; + +main2(["chunk-read-client"]) -> + io:format("Use: Read a series of chunks for a single server.\n"), + io:format("Args: Host Port LocalChunkDescriptionPath [OutputPath|'console']\n"), + erlang:halt(1); +main2(["chunk-read-client", Host, PortStr, ChunkFileList]) -> + main2(["chunk-read-client", Host, PortStr, ChunkFileList, "console"]); +main2(["chunk-read-client", Host, PortStr, ChunkFileList, OutputPath]) -> + FH = open_output_file(OutputPath), + OutFun = make_outfun(FH), + try + main2(["chunk-read-client2", Host, PortStr, ChunkFileList, OutFun]) + after + (catch file:close(FH)) + end; +main2(["chunk-read-client2", Host, PortStr, ChunkFileList, ProcFun]) -> + Sock = escript_connect(Host, PortStr), + escript_download_chunks(Sock, ChunkFileList, ProcFun); + +main2(["delete-client"]) -> + io:format("Use: Delete a file (NOT FOR GENERAL USE)\n"), + io:format("Args: Host Port File\n"), + erlang:halt(1); +main2(["delete-client", Host, PortStr, File]) -> + Sock = escript_connect(Host, PortStr), + escript_delete(Sock, File); + +%%%% cc flavors %%%% + +main2(["cc-1file-write-redundant-client"]) -> + io:format("Use: Write a local file to a chain via projection.\n"), + io:format("Args: BlockSize Prefix LocalFilePath ProjectionPath\n"), + erlang:halt(1); +main2(["cc-1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile, ProjectionPath]) -> + BlockSize = list_to_integer(BlockSizeStr), + Prefix = list_to_binary(PrefixStr), + {_Chain, RawHPs} = calc_chain(write, ProjectionPath, PrefixStr), + HPs = convert_raw_hps(RawHPs), + Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile), + print_upload_details(user, Res), + Res; + +main2(["cc-chunk-read-client"]) -> + io:format("Use: Read a series of chunks from a chain via projection.\n"), + io:format("Args: ProjectionPath ChunkFileList [OutputPath|'console' \\\n\t[ErrorCorrection_ProjectionPath]]\n"), + erlang:halt(1); +main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList]) -> + main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList,"console", + undefined]); +main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath]) -> + main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, + undefined]); +main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, + EC_ProjectionPath]) -> + main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, + EC_ProjectionPath]). + +main3(["cc-chunk-read-client", + ProjectionPathOrDir, ChunkFileList, OutputPath, EC_ProjectionPath]) -> + P = read_projection_file(ProjectionPathOrDir), + ChainMap = read_chain_map_file(ProjectionPathOrDir), + FH = open_output_file(OutputPath), + ProcFun = make_outfun(FH), + Res = try + escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun, + EC_ProjectionPath) + after + (catch file:close(FH)) + end, + Res. + +-spec connect(inet:ip_address() | inet:hostname(), inet:port_number()) -> + port(). +connect(Host, Port) -> + escript_connect(Host, Port). + +escript_connect(Host, PortStr) when is_list(PortStr) -> + Port = list_to_integer(PortStr), + escript_connect(Host, Port); +escript_connect(Host, Port) when is_integer(Port) -> + {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}, {mode,binary}, + {packet, raw}]), + Sock. + +escript_upload_file(Sock, BlockSize, Prefix, File) -> + {ok, FH} = file:open(File, [read, raw, binary]), + try + escript_upload_file2(file:read(FH, BlockSize), FH, + BlockSize, Prefix, Sock, []) + after + file:close(FH) + end. + +escript_upload_file2({ok, Chunk}, FH, BlockSize, Prefix, Sock, Acc) -> + {OffsetHex, LenHex, File} = upload_chunk_append(Sock, Prefix, Chunk), + verb("~s ~s ~s\n", [OffsetHex, LenHex, File]), + <> = hexstr_to_bin(OffsetHex), + <> = hexstr_to_bin(LenHex), + OSF = {Offset, Size, File}, + escript_upload_file2(file:read(FH, BlockSize), FH, BlockSize, Prefix, Sock, + [OSF|Acc]); +escript_upload_file2(eof, _FH, _BlockSize, _Prefix, _Sock, Acc) -> + lists:reverse(Acc). + +upload_chunk_append(Sock, Prefix, Chunk) -> + %% _ = crypto:hash(md5, Chunk), + Len = byte_size(Chunk), + LenHex = list_to_binary(bin_to_hexstr(<>)), + Cmd = <<"A ", LenHex/binary, " ", Prefix/binary, "\n">>, + ok = gen_tcp:send(Sock, [Cmd, Chunk]), + {ok, Line} = gen_tcp:recv(Sock, 0), + PathLen = byte_size(Line) - 3 - 16 - 1 - 1, + <<"OK ", OffsetHex:16/binary, " ", Path:PathLen/binary, _:1/binary>> = Line, + {OffsetHex, LenHex, Path}. + +upload_chunk_write(Sock, Offset, File, Chunk) when is_integer(Offset) -> + OffsetHex = list_to_binary(bin_to_hexstr(<>)), + upload_chunk_write(Sock, OffsetHex, File, Chunk); +upload_chunk_write(Sock, OffsetHex, File, Chunk) when is_binary(OffsetHex) -> + %% _ = crypto:hash(md5, Chunk), + Len = byte_size(Chunk), + LenHex = list_to_binary(bin_to_hexstr(<>)), + Cmd = <<"W-repl ", OffsetHex/binary, " ", + LenHex/binary, " ", File/binary, "\n">>, + ok = gen_tcp:send(Sock, [Cmd, Chunk]), + {ok, Line} = gen_tcp:recv(Sock, 0), + <<"OK\n">> = Line, + {OffsetHex, LenHex, File}. + +escript_upload_redundant([Host, PortStr|HPs], BlockSize, Prefix, LocalFile) -> + Sock = escript_connect(Host, PortStr), + ok = inet:setopts(Sock, [{packet, line}]), + OSFs = try + escript_upload_file(Sock, BlockSize, Prefix, LocalFile) + after + gen_tcp:close(Sock) + end, + escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs). + +escript_upload_redundant2([], _OSFs, _LocalFile, OSFs) -> + OSFs; +escript_upload_redundant2([Host, PortStr|HPs], OSFs, LocalFile, OSFs) -> + Sock = escript_connect(Host, PortStr), + {ok, FH} = file:open(LocalFile, [read, binary, raw]), + try + [begin + {ok, Chunk} = file:read(FH, Size), + _OSF2 = upload_chunk_write(Sock, Offset, File, Chunk) + %% verb("~p: ~p\n", [{Host, PortStr}, OSF2]) + end || {Offset, Size, File} <- OSFs] + after + gen_tcp:close(Sock), + file:close(FH) + end, + escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs). + +escript_download_chunks(Sock, {{{ChunkLine}}}, ProcFun) -> + escript_download_chunk({ok, ChunkLine}, invalid_fd, Sock, ProcFun); +escript_download_chunks(Sock, ChunkFileList, ProcFun) -> + {ok, FH} = file:open(ChunkFileList, [read, raw, binary]), + escript_download_chunk(file:read_line(FH), FH, Sock, ProcFun). + +escript_download_chunk({ok, Line}, FH, Sock, ProcFun) -> + ChunkOrError = escript_cc_download_chunk2(Sock, Line), + ProcFun(ChunkOrError), + [ChunkOrError| + escript_download_chunk((catch file:read_line(FH)), FH, Sock, ProcFun)]; +escript_download_chunk(eof, _FH, _Sock, ProcFun) -> + ProcFun(eof), + []; +escript_download_chunk(_Else, _FH, _Sock, ProcFun) -> + ProcFun(eof), + []. + +escript_cc_download_chunks({{{ChunkLine}}}, P, ChainMap, ProcFun, + EC_ProjectionPath) -> + escript_cc_download_chunk({ok,ChunkLine}, invalid_fd, P, ChainMap, ProcFun, + EC_ProjectionPath); +escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun, + EC_ProjectionPath) -> + {ok, FH} = file:open(ChunkFileList, [read, raw, binary]), + escript_cc_download_chunk(file:read_line(FH), FH, P, ChainMap, ProcFun, + EC_ProjectionPath). + +escript_cc_download_chunk({ok, Line}, FH, P, ChainMap, ProcFun, + EC_ProjectionPath) -> + RestLen = byte_size(Line) - 16 - 1 - 8 - 1 - 1, + <<_Offset:16/binary, " ", _Len:8/binary, " ", Rest:RestLen/binary, "\n">> + = Line, + Prefix = re:replace(Rest, "\\..*", "", [{return, binary}]), + {_Chains, RawHPs} = calc_chain(read, P, ChainMap, Prefix), + Chunk = lists:foldl( + fun(_RawHP, Bin) when is_binary(Bin) -> Bin; + (RawHP, _) -> + [Host, PortStr] = convert_raw_hps([RawHP]), + Sock = get_cached_sock(Host, PortStr), + case escript_cc_download_chunk2(Sock, Line) of + Bin when is_binary(Bin) -> + Bin; + {error, _} = Error -> + Error; + {erasure_encoded, _} = EC_info -> + escript_cc_download_ec_chunk(EC_info, + EC_ProjectionPath) + end + end, undefined, RawHPs), + ProcFun(Chunk), + [Chunk|escript_cc_download_chunk((catch file:read_line(FH)), + FH, P, ChainMap, ProcFun, + EC_ProjectionPath)]; +escript_cc_download_chunk(eof, _FH, _P, _ChainMap, ProcFun, + _EC_ProjectionPath) -> + ProcFun(eof), + []; +escript_cc_download_chunk(Else, _FH, _P, _ChainMap, ProcFun, + _EC_ProjectionPath) -> + ProcFun(Else), + []. + +escript_cc_download_chunk2(Sock, Line) -> + %% Line includes an LF, so we can be lazy. + CmdLF = [<<"R ">>, Line], + ok = gen_tcp:send(Sock, CmdLF), + case gen_tcp:recv(Sock, 3) of + {ok, <<"OK\n">>} -> + {_Offset, Size, _File} = read_hex_size(Line), + {ok, Chunk} = gen_tcp:recv(Sock, Size), + Chunk; + {ok, Else} -> + {ok, OldOpts} = inet:getopts(Sock, [packet]), + ok = inet:setopts(Sock, [{packet, line}]), + {ok, Else2} = gen_tcp:recv(Sock, 0), + ok = inet:setopts(Sock, OldOpts), + case Else of + <<"ERA">> -> + escript_cc_parse_ec_info(Sock, Line, Else2); + _ -> + {error, {Line, <>}} + end + end. + +escript_cc_parse_ec_info(Sock, Line, Else2) -> + ChompLine = chomp(Line), + {Offset, Size, File} = read_hex_size(ChompLine), + <<"SURE ", BodyLenHex:4/binary, " ", StripeWidthHex:16/binary, " ", + OrigFileLenHex:16/binary, " rs_10_4_v1", _/binary>> = Else2, + <> = hexstr_to_bin(BodyLenHex), + {ok, SummaryBody} = gen_tcp:recv(Sock, BodyLen), + + <> = hexstr_to_bin(StripeWidthHex), + <> = hexstr_to_bin(OrigFileLenHex), + NewFileNum = (Offset div StripeWidth) + 1, + NewOffset = Offset rem StripeWidth, + if Offset + Size > OrigFileLen -> + %% Client's request is larger than original file size, derp + {error, bad_offset_and_size}; + NewOffset + Size > StripeWidth -> + %% Client's request straddles a stripe boundary, TODO fix me + {error, todo_TODO_implement_this_with_two_reads_and_then_glue_together}; + true -> + NewOffsetHex = bin_to_hexstr(<>), + LenHex = bin_to_hexstr(<>), + NewSuffix = file_suffix_rs_10_4_v1(NewFileNum), + NewFile = iolist_to_binary([File, NewSuffix]), + NewLine = iolist_to_binary([NewOffsetHex, " ", LenHex, " ", + NewFile, "\n"]), + {erasure_encoded, {Offset, Size, File, NewOffset, NewFile, + NewFileNum, NewLine, SummaryBody}} + end. + +%% TODO: The EC method/version/type stuff here is loosey-goosey +escript_cc_download_ec_chunk(EC_info, undefined) -> + EC_info; +escript_cc_download_ec_chunk({erasure_encoded, + {_Offset, _Size, _File, _NewOffset, NewFile, + NewFileNum, NewLine, SummaryBody}}, + EC_ProjectionPath) -> + {P, ChainMap} = get_cached_projection(EC_ProjectionPath), + %% Remember: we use the whole file name for hashing, not the prefix + {_Chains, RawHPs} = calc_chain(read, P, ChainMap, NewFile), + RawHP = lists:nth(NewFileNum, RawHPs), + [Host, PortStr] = convert_raw_hps([RawHP]), + Sock = get_cached_sock(Host, PortStr), + case escript_cc_download_chunk2(Sock, NewLine) of + Chunk when is_binary(Chunk) -> + Chunk; + {error, _} = Else -> + io:format("TODO: EC chunk get failed:\n\t~s\n", [NewLine]), + io:format("Use this info to reconstruct:\n\t~p\n\n", [SummaryBody]), + Else + end. + +get_cached_projection(EC_ProjectionPath) -> + case get(cached_projection) of + undefined -> + P = read_projection_file(EC_ProjectionPath), + ChainMap = read_chain_map_file(EC_ProjectionPath), + put(cached_projection, {P, ChainMap}), + get_cached_projection(EC_ProjectionPath); + Stuff -> + Stuff + end. + +file_suffix_rs_10_4_v1(1) -> <<"_k01">>; +file_suffix_rs_10_4_v1(2) -> <<"_k02">>; +file_suffix_rs_10_4_v1(3) -> <<"_k03">>; +file_suffix_rs_10_4_v1(4) -> <<"_k04">>; +file_suffix_rs_10_4_v1(5) -> <<"_k05">>; +file_suffix_rs_10_4_v1(6) -> <<"_k06">>; +file_suffix_rs_10_4_v1(7) -> <<"_k07">>; +file_suffix_rs_10_4_v1(8) -> <<"_k08">>; +file_suffix_rs_10_4_v1(9) -> <<"_k09">>; +file_suffix_rs_10_4_v1(10) -> <<"_k10">>. + +escript_delete(Sock, File) -> + ok = gen_tcp:send(Sock, [<<"DEL-migration ">>, File, <<"\n">>]), + ok = inet:setopts(Sock, [{packet, line}]), + case gen_tcp:recv(Sock, 0) of + {ok, <<"OK\n">>} -> + ok; + {ok, <<"ERROR", _/binary>>} -> + error + end. + +escript_compare_servers(Sock1, Sock2, H1, H2, Args) -> + FileFilterFun = fun(_) -> true end, + escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args). + +escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args) -> + All = [H1, H2], + put(mydict, dict:new()), + Fetch1 = make_fetcher(H1), + Fetch2 = make_fetcher(H2), + + Fmt = case Args of + [] -> + fun(eof) -> ok; (Str) -> io:format(user, Str, []) end; + [null] -> + fun(_) -> ok end; + [OutFile] -> + {ok, FH} = file:open(OutFile, [write]), + fun(eof) -> file:close(FH); + (Str) -> file:write(FH, Str) + end + end, + + %% TODO: Broken! Fetch1 and Fetch2 aren't created when comments are below + Sock1=Sock1,Sock2=Sock2,Fetch1=Fetch1,Fetch2=Fetch2, % shut up compiler + %% _X1 = escript_list2(Sock1, Fetch1), + %% _X2 = escript_list2(Sock2, Fetch2), + FoldRes = lists:sort(dict:to_list(get(mydict))), + Fmt("{legend, {file, list_of_servers_without_file}}.\n"), + Fmt(io_lib:format("{all, ~p}.\n", [All])), + Res = [begin + {GotIt, Sizes} = lists:unzip(GotSizes), + Size = lists:max(Sizes), + Missing = {File, {Size, All -- GotIt}}, + verb("~p.\n", [Missing]), + Missing + end || {File, GotSizes} <- FoldRes, FileFilterFun(File)], + (catch Fmt(eof)), + Res. + +make_fetcher(Host) -> + fun(eof) -> + ok; + (<>) -> + <> = hexstr_to_bin(SizeHex), + FileLen = byte_size(Rest) - 1, + <> = Rest, + NewDict = dict:append(File, {Host, Size}, get(mydict)), + put(mydict, NewDict) + end. + +checksum(Bin) when is_binary(Bin) -> + crypto:hash(md5, Bin). + +verb(Fmt) -> + verb(Fmt, []). + +verb(Fmt, Args) -> + case application:get_env(kernel, verbose) of + {ok, true} -> io:format(Fmt, Args); + _ -> ok + end. + +info_msg(Fmt, Args) -> + case application:get_env(kernel, verbose) of {ok, false} -> ok; + _ -> error_logger:info_msg(Fmt, Args) + end. + +repair(File, Size, [], Mode, V, SrcS, SrcS2, DstS, DstS2, _Src) -> + verb("~s: present on both: ", [File]), + repair_both_present(File, Size, Mode, V, SrcS, SrcS2, DstS, DstS2); +repair(File, Size, MissingList, Mode, V, SrcS, SrcS2, DstS, _DstS2, Src) -> + case lists:member(Src, MissingList) of + true -> + verb("~s -> ~p, skipping: not on source server\n", [File, MissingList]); + false when Mode == check -> + verb("~s -> ~p, copy ~s MB (skipped)\n", [File, MissingList, mbytes(Size)]); + false -> + verb("~s -> ~p, copy ~s MB ", [File, MissingList, mbytes(Size)]), + ok = copy_file(File, SrcS, SrcS2, DstS, V), + verb("done\n", []) + end. + +copy_file(File, SrcS, SrcS2, DstS, Verbose) -> + %% Use the *second* source socket to copy each chunk. + ProcChecksum = copy_file_proc_checksum_fun(File, SrcS2, DstS, Verbose), + %% Use the *first source socket to enumerate the chunks & checksums. + exit(todo_broken), + machi_flu1_client:checksum_list(SrcS, File, line_by_line, ProcChecksum). + +copy_file_proc_checksum_fun(File, SrcS, DstS, _Verbose) -> + fun(<>) -> + <> = hexstr_to_bin(LenHex), + DownloadChunkBin = <>, + [Chunk] = escript_download_chunks(SrcS, {{{DownloadChunkBin}}}, + fun(_) -> ok end), + CSum = hexstr_to_bin(CSumHex), + CSum2 = checksum(Chunk), + if Len == byte_size(Chunk), CSum == CSum2 -> + {_,_,_} = upload_chunk_write(DstS, OffsetHex, File, Chunk), + ok; + true -> + io:format("ERROR: ~s ~s ~s csum/size error\n", + [File, OffsetHex, LenHex]), + error + end; + (_Else) -> + ok + end. + +repair_both_present(File, Size, Mode, V, SrcS, _SrcS2, DstS, _DstS2) -> + Tmp1 = lists:flatten(io_lib:format("/tmp/sort.1.~w.~w.~w", tuple_to_list(now()))), + Tmp2 = lists:flatten(io_lib:format("/tmp/sort.2.~w.~w.~w", tuple_to_list(now()))), + J_Both = lists:flatten(io_lib:format("/tmp/join.3-both.~w.~w.~w", tuple_to_list(now()))), + J_SrcOnly = lists:flatten(io_lib:format("/tmp/join.4-src-only.~w.~w.~w", tuple_to_list(now()))), + J_DstOnly = lists:flatten(io_lib:format("/tmp/join.5-dst-only.~w.~w.~w", tuple_to_list(now()))), + S_Identical = lists:flatten(io_lib:format("/tmp/join.6-sort-identical.~w.~w.~w", tuple_to_list(now()))), + {ok, FH1} = file:open(Tmp1, [write, raw, binary]), + {ok, FH2} = file:open(Tmp2, [write, raw, binary]), + try + K = md5_ctx, + MD5_it = fun(Bin) -> + {FH, MD5ctx1} = get(K), + file:write(FH, Bin), + MD5ctx2 = crypto:hash_update(MD5ctx1, Bin), + put(K, {FH, MD5ctx2}) + end, + put(K, {FH1, crypto:hash_init(md5)}), + exit(todo_broken), + ok = machi_flu1_client:checksum_list(SrcS, File, fast, MD5_it), + {_, MD5_1} = get(K), + SrcMD5 = crypto:hash_final(MD5_1), + put(K, {FH2, crypto:hash_init(md5)}), + exit(todo_broken), + ok = machi_flu1_client:checksum_list(DstS, File, fast, MD5_it), + {_, MD5_2} = get(K), + DstMD5 = crypto:hash_final(MD5_2), + if SrcMD5 == DstMD5 -> + verb("identical\n", []); + true -> + ok = file:close(FH1), + ok = file:close(FH2), + _Q1 = os:cmd("./REPAIR-SORT-JOIN.sh " ++ Tmp1 ++ " " ++ Tmp2 ++ " " ++ J_Both ++ " " ++ J_SrcOnly ++ " " ++ J_DstOnly ++ " " ++ S_Identical), + case file:read_file_info(S_Identical) of + {ok, _} -> + verb("identical (secondary sort)\n", []); + {error, enoent} -> + io:format("differences found:"), + repair_both(File, Size, V, Mode, + J_Both, J_SrcOnly, J_DstOnly, + SrcS, DstS) + end + end + after + catch file:close(FH1), + catch file:close(FH2), + [(catch file:delete(FF)) || FF <- [Tmp1,Tmp2,J_Both,J_SrcOnly,J_DstOnly, + S_Identical]] + end. + +repair_both(File, _Size, V, Mode, J_Both, J_SrcOnly, J_DstOnly, SrcS, DstS) -> + AccFun = if Mode == check -> + fun(_X, List) -> List end; + Mode == repair -> + fun( X, List) -> [X|List] end + end, + BothFun = fun(<<_OffsetSrcHex:16/binary, " ", + LenSrcHex:8/binary, " ", CSumSrcHex:32/binary, " ", + LenDstHex:8/binary, " ", CSumDstHex:32/binary, "\n">> =Line, + {SameB, SameC, DiffB, DiffC, Ds}) -> + <> = hexstr_to_bin(LenSrcHex), + if LenSrcHex == LenDstHex, + CSumSrcHex == CSumDstHex -> + {SameB + Len, SameC + 1, DiffB, DiffC, Ds}; + true -> + %% D = {OffsetSrcHex, LenSrcHex, ........ + {SameB, SameC, DiffB + Len, DiffC + 1, + AccFun(Line, Ds)} + end; + (_Else, Acc) -> + Acc + end, + OnlyFun = fun(<<_OffsetSrcHex:16/binary, " ", LenSrcHex:8/binary, " ", + _CSumHex:32/binary, "\n">> = Line, + {DiffB, DiffC, Ds}) -> + <> = hexstr_to_bin(LenSrcHex), + {DiffB + Len, DiffC + 1, AccFun(Line, Ds)}; + (_Else, Acc) -> + Acc + end, + {SameBx, SameCx, DiffBy, DiffCy, BothDiffs} = + file_folder(BothFun, {0,0,0,0,[]}, J_Both), + {DiffB_src, DiffC_src, Ds_src} = file_folder(OnlyFun, {0,0,[]}, J_SrcOnly), + {DiffB_dst, DiffC_dst, Ds_dst} = file_folder(OnlyFun, {0,0,[]}, J_DstOnly), + if Mode == check orelse V == true -> + io:format("\n\t"), + io:format("BothR ~p, ", [{SameBx, SameCx, DiffBy, DiffCy}]), + io:format("SrcR ~p, ", [{DiffB_src, DiffC_src}]), + io:format("DstR ~p", [{DiffB_dst, DiffC_dst}]), + io:format("\n"); + true -> ok + end, + if Mode == repair -> + ok = repair_both_both(File, V, BothDiffs, SrcS, DstS), + ok = repair_copy_chunks(File, V, Ds_src, DiffB_src, DiffC_src, + SrcS, DstS), + ok = repair_copy_chunks(File, V, Ds_dst, DiffB_dst, DiffC_dst, + DstS, SrcS); + true -> + ok + end. + +repair_both_both(_File, _V, [_|_], _SrcS, _DstS) -> + %% TODO: fetch both, check checksums, hopefully only exactly one + %% is correct, then use that one to repair the other. And if the + %% sizes are different, hrm, there may be an extra corner case(s) + %% hiding there. + io:format("WHOA! We have differing checksums or sizes here, TODO not implemented, but there's trouble in the little village on the river....\n"), + timer:sleep(3*1000), + ok; +repair_both_both(_File, _V, [], _SrcS, _DstS) -> + ok. + +repair_copy_chunks(_File, _V, [], _DiffBytes, _DiffCount, _SrcS, _DstS) -> + ok; +repair_copy_chunks(File, V, ToBeCopied, DiffBytes, DiffCount, SrcS, DstS) -> + verb("\n", []), + verb("Starting copy of ~p chunks/~s MBytes to \n ~s: ", + [DiffCount, mbytes(DiffBytes), File]), + InnerCopyFun = copy_file_proc_checksum_fun(File, SrcS, DstS, V), + FoldFun = fun(Line, ok) -> + ok = InnerCopyFun(Line) % Strong sanity check + end, + ok = lists:foldl(FoldFun, ok, ToBeCopied), + verb(" done\n", []), + ok. + +file_folder(Fun, Acc, Path) -> + {ok, FH} = file:open(Path, [read, raw, binary]), + try + file_folder2(Fun, Acc, FH) + after + file:close(FH) + end. + +file_folder2(Fun, Acc, FH) -> + file_folder2(file:read_line(FH), Fun, Acc, FH). + +file_folder2({ok, Line}, Fun, Acc, FH) -> + Acc2 = Fun(Line, Acc), + file_folder2(Fun, Acc2, FH); +file_folder2(eof, _Fun, Acc, _FH) -> + Acc. + +make_repair_props(["check"|T]) -> + [{mode, check}|make_repair_props(T)]; +make_repair_props(["repair"|T]) -> + [{mode, repair}|make_repair_props(T)]; +make_repair_props(["verbose"|T]) -> + application:set_env(kernel, verbose, true), + [{verbose, true}|make_repair_props(T)]; +make_repair_props(["noverbose"|T]) -> + [{verbose, false}|make_repair_props(T)]; +make_repair_props(["progress"|T]) -> + [{progress, true}|make_repair_props(T)]; +make_repair_props(["delete-source"|T]) -> + [{delete_source, true}|make_repair_props(T)]; +make_repair_props(["nodelete-source"|T]) -> + [{delete_source, false}|make_repair_props(T)]; +make_repair_props(["nodelete-tmp"|T]) -> + [{delete_tmp, false}|make_repair_props(T)]; +make_repair_props([X|T]) -> + io:format("Error: skipping unknown option ~p\n", [X]), + make_repair_props(T); +make_repair_props([]) -> + %% Proplist defaults + [{mode, check}, {delete_source, false}]. + +mbytes(0) -> + "0.0"; +mbytes(Size) -> + lists:flatten(io_lib:format("~.1.0f", [max(0.1, Size / (1024*1024))])). + +chomp(Line) when is_binary(Line) -> + LineLen = byte_size(Line) - 1, + <> = Line, + ChompLine. + +make_outfun(FH) -> + fun({error, _} = Error) -> + file:write(FH, io_lib:format("Error: ~p\n", [Error])); + (eof) -> + ok; + ({erasure_encoded, Info} = _Erasure) -> + file:write(FH, "TODO/WIP: erasure_coded:\n"), + file:write(FH, io_lib:format("\t~p\n", [Info])); + (Bytes) when is_binary(Bytes) orelse is_list(Bytes) -> + file:write(FH, Bytes) + end. + +open_output_file("console") -> + user; +open_output_file(Path) -> + {ok, FH} = file:open(Path, [write]), + FH. + +print_upload_details(_, {error, _} = Res) -> + io:format("Error: ~p\n", [Res]), + erlang:halt(1); +print_upload_details(FH, Res) -> + [io:format(FH, "~s ~s ~s\n", [bin_to_hexstr(<>), + bin_to_hexstr(<>), + File]) || + {Offset, Len, File} <- Res]. + +%%%%%%%%%%%%%%%%% + +read_projection_file("new") -> + #projection{epoch=0, last_epoch=0, + float_map=undefined, last_float_map=undefined}; +read_projection_file(Path) -> + case filelib:is_dir(Path) of + true -> + read_projection_file_loop(Path ++ "/current.proj"); + false -> + case filelib:is_file(Path) of + true -> + read_projection_file2(Path); + false -> + error({bummer, Path}) + end + end. + +read_projection_file2(Path) -> + {ok, [P]} = file:consult(Path), + true = is_record(P, projection), + FloatMap = P#projection.float_map, + LastFloatMap = if P#projection.last_float_map == undefined -> + FloatMap; + true -> + P#projection.last_float_map + end, + P#projection{migrating=(FloatMap /= LastFloatMap), + tree=machi_chash:make_tree(FloatMap), + last_tree=machi_chash:make_tree(LastFloatMap)}. + +read_projection_file_loop(Path) -> + read_projection_file_loop(Path, 100). + +read_projection_file_loop(Path, 0) -> + error({bummer, Path}); +read_projection_file_loop(Path, N) -> + try + read_projection_file2(Path) + catch + error:{badmatch,{error,enoent}} -> + timer:sleep(100), + read_projection_file_loop(Path, N-1) + end. + +write_projection(P, Path) when is_record(P, projection) -> + {error, enoent} = file:read_file_info(Path), + {ok, FH} = file:open(Path, [write]), + WritingP = P#projection{tree=undefined, last_tree=undefined}, + io:format(FH, "~p.\n", [WritingP]), + ok = file:close(FH). + +read_weight_map_file(Path) -> + {ok, [Map]} = file:consult(Path), + true = is_list(Map), + true = lists:all(fun({Chain, Weight}) + when is_binary(Chain), + is_integer(Weight), Weight >= 0 -> + true; + (_) -> + false + end, Map), + Map. + +%% Assume the file "chains.map" in whatever dir that stores projections. +read_chain_map_file(DirPath) -> + L = case filelib:is_dir(DirPath) of + true -> + {ok, Map} = file:consult(DirPath ++ "/chains.map"), + Map; + false -> + Dir = filename:dirname(DirPath), + {ok, Map} = file:consult(Dir ++ "/chains.map"), + Map + end, + orddict:from_list(L). + +get_float_map(P) when is_record(P, projection) -> + P#projection.float_map. + +get_last_float_map(P) when is_record(P, projection) -> + P#projection.last_float_map. + +hash_and_query(Key, P) when is_record(P, projection) -> + <> = crypto:hash(sha, Key), + Float = Int / ?SHA_MAX, + {_, Current} = machi_chash:query_tree(Float, P#projection.tree), + if P#projection.migrating -> + {_, Last} = machi_chash:query_tree(Float, P#projection.last_tree), + if Last == Current -> + [Current]; + true -> + [Current, Last, Current] + end; + true -> + [Current] + end. + +calc_chain(write=Op, ProjectionPathOrDir, PrefixStr) -> + P = read_projection_file(ProjectionPathOrDir), + ChainMap = read_chain_map_file(ProjectionPathOrDir), + calc_chain(Op, P, ChainMap, PrefixStr); +calc_chain(read=Op, ProjectionPathOrDir, PrefixStr) -> + P = read_projection_file(ProjectionPathOrDir), + ChainMap = read_chain_map_file(ProjectionPathOrDir), + calc_chain(Op, P, ChainMap, PrefixStr). + +calc_chain(write=_Op, P, ChainMap, PrefixStr) -> + %% Writes are easy: always use the new location. + [Chain|_] = hash_and_query(PrefixStr, P), + {Chain, orddict:fetch(Chain, ChainMap)}; +calc_chain(read=_Op, P, ChainMap, PrefixStr) -> + %% Reads are slightly trickier: reverse each chain so tail is tried first. + Chains = hash_and_query(PrefixStr, P), + {Chains, lists:flatten([lists:reverse(orddict:fetch(Chain, ChainMap)) || + Chain <- Chains])}. + +convert_raw_hps([{HostBin, Port}|T]) -> + [binary_to_list(HostBin), integer_to_list(Port)|convert_raw_hps(T)]; +convert_raw_hps([]) -> + []. + +get_cached_sock(Host, PortStr) -> + K = {socket_cache, Host, PortStr}, + case erlang:get(K) of + undefined -> + Sock = escript_connect(Host, PortStr), + Krev = {socket_cache_rev, Sock}, + erlang:put(K, Sock), + erlang:put(Krev, {Host, PortStr}), + Sock; + Sock -> + Sock + end. + +invalidate_cached_sock(Sock) -> + (catch gen_tcp:close(Sock)), + Krev = {socket_cache_rev, Sock}, + case erlang:get(Krev) of + undefined -> + ok; + {Host, PortStr} -> + K = {socket_cache, Host, PortStr}, + erlang:erase(Krev), + erlang:erase(K), + ok + end. + +%%%%%%%%%%%%%%%%% + +%%% basho_bench callbacks + +-define(SEQ, ?MODULE). +-define(DEFAULT_HOSTIP_LIST, [{{127,0,0,1}, 7071}]). + +-record(bb, { + host, + port_str, + %% sock, + proj_check_ticker_started=false, + proj_path, + proj, + chain_map + }). + +new(1 = Id) -> + %% broken: start_append_server(), + case basho_bench_config:get(file0_start_listener, no) of + no -> + ok; + {_Port, _DataDir} -> + exit(todo_broken) + end, + timer:sleep(100), + new_common(Id); +new(Id) -> + new_common(Id). + +new_common(Id) -> + random:seed(now()), + ProjectionPathOrDir = + basho_bench_config:get(file0_projection_path, undefined), + + Servers = basho_bench_config:get(file0_ip_list, ?DEFAULT_HOSTIP_LIST), + NumServers = length(Servers), + {Host, Port} = lists:nth((Id rem NumServers) + 1, Servers), + State0 = #bb{host=Host, port_str=integer_to_list(Port), + proj_path=ProjectionPathOrDir}, + {ok, read_projection_info(State0)}. + +run(null, _KeyGen, _ValueGen, State) -> + {ok, State}; +run(keygen_valuegen_then_null, KeyGen, ValueGen, State) -> + _Prefix = KeyGen(), + _Value = ValueGen(), + {ok, State}; +run(append_local_server, KeyGen, ValueGen, State) -> + Prefix = KeyGen(), + Value = ValueGen(), + {_, _} = ?SEQ:append(?SEQ, Prefix, Value), + {ok, State}; +run(append_remote_server, KeyGen, ValueGen, State) -> + Prefix = KeyGen(), + Value = ValueGen(), + bb_do_write_chunk(Prefix, Value, State#bb.host, State#bb.port_str, State); +run(cc_append_remote_server, KeyGen, ValueGen, State0) -> + State = check_projection_check(State0), + Prefix = KeyGen(), + Value = ValueGen(), + {_Chain, ModHPs} = calc_chain(write, State#bb.proj, State#bb.chain_map, + Prefix), + FoldFun = fun({Host, PortStr}, Acc) -> + case bb_do_write_chunk(Prefix, Value, Host, PortStr, + State) of + {ok, _} -> + Acc + 1; + _ -> + Acc + end + end, + case lists:foldl(FoldFun, 0, ModHPs) of + N when is_integer(N), N > 0 -> + {ok, State}; + 0 -> + {error, oh_some_problem_yo, State} + end; +run(read_raw_line_local, KeyGen, _ValueGen, State) -> + {RawLine, Size, _File} = setup_read_raw_line(KeyGen), + bb_do_read_chunk(RawLine, Size, State#bb.host, State#bb.port_str, State); +run(cc_read_raw_line_local, KeyGen, _ValueGen, State0) -> + State = check_projection_check(State0), + {RawLine, Size, File} = setup_read_raw_line(KeyGen), + Prefix = re:replace(File, "\\..*", "", [{return, binary}]), + {_Chain, ModHPs} = calc_chain(read, State#bb.proj, State#bb.chain_map, + Prefix), + FoldFun = fun(_, {ok, _}=Acc) -> + Acc; + ({Host, PortStr}, _Acc) -> + bb_do_read_chunk(RawLine, Size, Host, PortStr, State) + end, + lists:foldl(FoldFun, undefined, ModHPs). + +bb_do_read_chunk(RawLine, Size, Host, PortStr, State) -> + try + Sock = get_cached_sock(Host, PortStr), + try + ok = gen_tcp:send(Sock, [RawLine, <<"\n">>]), + read_chunk(Sock, Size, State) + catch X2:Y2 -> + invalidate_cached_sock(Sock), + {error, {X2,Y2}, State} + end + catch X:Y -> + {error, {X,Y}, State} + end. + +bb_do_write_chunk(Prefix, Value, Host, PortStr, State) -> + try + Sock = get_cached_sock(Host, PortStr), + try + {_, _, _} = upload_chunk_append(Sock, Prefix, Value), + {ok, State} + catch X2:Y2 -> + invalidate_cached_sock(Sock), + {error, {X2,Y2}, State} + end + catch X:Y -> + {error, {X,Y}, State} + end. + +read_chunk(Sock, Size, State) -> + {ok, <<"OK\n">>} = gen_tcp:recv(Sock, 3), + {ok, _Chunk} = gen_tcp:recv(Sock, Size), + {ok, State}. + +setup_read_raw_line(KeyGen) -> + RawLine = KeyGen(), + <<"R ", Rest/binary>> = RawLine, + {_Offset, Size, File} = read_hex_size(Rest), + {RawLine, Size, File}. + +read_hex_size(Line) -> + <> = Line, + <> = hexstr_to_bin(OffsetHex), + <> = hexstr_to_bin(SizeHex), + {Offset, Size, File}. + +read_projection_info(#bb{proj_path=undefined}=State) -> + State; +read_projection_info(#bb{proj_path=ProjectionPathOrDir}=State) -> + Proj = read_projection_file(ProjectionPathOrDir), + ChainMap = read_chain_map_file(ProjectionPathOrDir), + ModChainMap = + [{Chain, [{binary_to_list(Host), integer_to_list(Port)} || + {Host, Port} <- Members]} || + {Chain, Members} <- ChainMap], + State#bb{proj=Proj, chain_map=ModChainMap}. + +check_projection_check(#bb{proj_check_ticker_started=false} = State) -> + timer:send_interval(5*1000 - random:uniform(500), projection_check), + check_projection_check(State#bb{proj_check_ticker_started=true}); +check_projection_check(#bb{proj_check_ticker_started=true} = State) -> + receive + projection_check -> + read_projection_info(State) + after 0 -> + State + end. diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl new file mode 100644 index 0000000..d51143c --- /dev/null +++ b/test/machi_flu1_test.erl @@ -0,0 +1,98 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_flu1_test). +-compile(export_all). + +-ifdef(TEST). +-include("machi.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(FLU, machi_flu1). +-define(FLU_C, machi_flu1_client). + +flu_smoke_test() -> + Host = "localhost", + TcpPort = 32957, + DataDir = "./data", + Prefix = <<"prefix!">>, + BadPrefix = BadFile = "no/good", + clean_up_data_dir(DataDir), + + {ok, FLU1} = ?FLU:start_link([{smoke_flu, TcpPort, DataDir}]), + try + {error, no_such_file} = ?FLU_C:checksum_list(Host, TcpPort, + "does-not-exist"), + {error, bad_arg} = ?FLU_C:checksum_list(Host, TcpPort, BadFile), + + {ok, []} = ?FLU_C:list_files(Host, TcpPort), + + Chunk1 = <<"yo!">>, + {ok, {Off1,Len1,File1}} = ?FLU_C:append_chunk(Host, TcpPort, + Prefix, Chunk1), + {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, File1, Off1, Len1), + {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, + BadPrefix, Chunk1), + + Chunk2 = <<"yo yo">>, + Len2 = byte_size(Chunk2), + Off2 = ?MINIMUM_OFFSET + 77, + File2 = "smoke-file", + ok = ?FLU_C:write_chunk(Host, TcpPort, File2, Off2, Chunk2), + {error, bad_arg} = ?FLU_C:write_chunk(Host, TcpPort, + BadFile, Off2, Chunk2), + {ok, Chunk2} = ?FLU_C:read_chunk(Host, TcpPort, File2, Off2, Len2), + {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, + File2, Off2*983, Len2), + {error, partial_read} = ?FLU_C:read_chunk(Host, TcpPort, + File2, Off2, Len2*984), + {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, + "no!!", Off2, Len2), + {error, bad_arg} = ?FLU_C:read_chunk(Host, TcpPort, + BadFile, Off2, Len2), + + %% We know that File1 still exists. Pretend that we've done a + %% migration and exercise the delete_migration() API. + ok = ?FLU_C:delete_migration(Host, TcpPort, File1), + {error, no_such_file} = ?FLU_C:delete_migration(Host, TcpPort, File1), + {error, bad_arg} = ?FLU_C:delete_migration(Host, TcpPort, BadFile), + + %% We know that File2 still exists. Pretend that we've done a + %% migration and exercise the trunc_hack() API. + ok = ?FLU_C:trunc_hack(Host, TcpPort, File2), + ok = ?FLU_C:trunc_hack(Host, TcpPort, File2), + {error, bad_arg} = ?FLU_C:trunc_hack(Host, TcpPort, BadFile), + + ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) + after + ok = ?FLU:stop(FLU1) + end. + +clean_up_data_dir(DataDir) -> + Dir1 = DataDir ++ "/config", + Fs1 = filelib:wildcard(Dir1 ++ "/*"), + [file:delete(F) || F <- Fs1], + _ = file:del_dir(Dir1), + Fs2 = filelib:wildcard(DataDir ++ "/*"), + [file:delete(F) || F <- Fs2], + _ = file:del_dir(DataDir), + ok. + +-endif. % TEST From 5c20ee633738bb0fa61163a4a25c798b86f1247a Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Wed, 1 Apr 2015 17:59:40 +0900 Subject: [PATCH 02/17] Fix client API for file list & checksum list --- src/machi_admin_util.erl | 39 +++++++++++++++++++++++++++++++++++++++ src/machi_flu1.erl | 39 ++++++++++++++++++++++++++++++++++++--- src/machi_flu1_client.erl | 23 +++++++++++++++++++++-- test/machi_flu1_test.erl | 22 +++++++++++++++------- 4 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/machi_admin_util.erl diff --git a/src/machi_admin_util.erl b/src/machi_admin_util.erl new file mode 100644 index 0000000..6a9bdc7 --- /dev/null +++ b/src/machi_admin_util.erl @@ -0,0 +1,39 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_admin_util). + +-export([ + verify_file_checksums_remote/2, verify_file_checksums_remote/3 + ]). +-compile(export_all). + +-include("machi.hrl"). + +verify_file_checksums_remote(Sock, File) -> + verify_file_checksums_remote2(Sock, Sock, File). + +verify_file_checksums_remote(_Host, _TcpPort, File) -> + verify_file_checksums_remote2(todo, todo, File). + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +verify_file_checksums_remote2(Sock, Sock, File) -> + todo. diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index d78de9d..5a5f04e 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -311,6 +311,8 @@ do_net_server_checksum_listing(Sock, File, DataDir) -> end. do_net_server_checksum_listing2(Sock, File, DataDir) -> + ok = sync_checksum_file(File), + CSumPath = machi_util:make_checksum_filename(DataDir, File), case file:open(CSumPath, [read, raw, binary]) of {ok, FH} -> @@ -330,6 +332,29 @@ do_net_server_checksum_listing2(Sock, File, DataDir) -> ok = gen_tcp:send(Sock, "ERROR\n") end. +sync_checksum_file(File) -> + Prefix = re:replace(File, "\\..*", "", [{return, binary}]), + case write_server_find_pid(Prefix) of + undefined -> + ok; + Pid -> + Ref = make_ref(), + Pid ! {sync_stuff, self(), Ref}, + receive + {sync_finished, Ref} -> + ok + after 5000 -> + case write_server_find_pid(Prefix) of + undefined -> + ok; + Pid2 when Pid2 /= Pid -> + ok; + _Pid2 -> + error + end + end + end. + do_net_copy_bytes(FH, Sock) -> case file:read(FH, 1024*1024) of {ok, Bin} -> @@ -384,8 +409,7 @@ do_net_server_truncate_hackityhack2(Sock, File, DataDir) -> end. write_server_get_pid(Prefix, DataDir) -> - RegName = machi_util:make_regname(Prefix), - case whereis(RegName) of + case write_server_find_pid(Prefix) of undefined -> start_seq_append_server(Prefix, DataDir), timer:sleep(1), @@ -394,6 +418,10 @@ write_server_get_pid(Prefix, DataDir) -> Pid end. +write_server_find_pid(Prefix) -> + RegName = machi_util:make_regname(Prefix), + whereis(RegName). + start_seq_append_server(Prefix, DataDir) -> spawn_link(fun() -> run_seq_append_server(Prefix, DataDir) end). @@ -453,7 +481,12 @@ seq_append_server_loop(DataDir, Prefix, File, {FHd,FHc}=FH_, FileNum, Offset) -> CSum_info = [OffsetHex, 32, LenHex, 32, CSumHex, 10], ok = file:write(FHc, CSum_info), seq_append_server_loop(DataDir, Prefix, File, FH_, - FileNum, Offset + Len) + FileNum, Offset + Len); + {sync_stuff, FromPid, Ref} -> + file:sync(FHc), + FromPid ! {sync_finished, Ref}, + seq_append_server_loop(DataDir, Prefix, File, FH_, + FileNum, Offset) after 30*1000 -> ok = file:close(FHd), ok = file:close(FHc), diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 3cccf00..7e8bc1b 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -285,7 +285,10 @@ list2(Sock) -> list2({ok, <<".\n">>}, _Sock) -> []; list2({ok, Line}, Sock) -> - [Line|list2(gen_tcp:recv(Sock, 0), Sock)]; + FileLen = byte_size(Line) - 16 - 1 - 1, + <> = Line, + Size = machi_util:hexstr_to_int(SizeHex), + [{Size, File}|list2(gen_tcp:recv(Sock, 0), Sock)]; list2(Else, _Sock) -> throw({server_protocol_error, Else}). @@ -300,7 +303,7 @@ checksum_list2(Sock, File) -> <> = Rest, <> = machi_util:hexstr_to_bin(LenHex), ok = inet:setopts(Sock, [{packet, raw}]), - checksum_list_fast(Sock, Len); + {ok, checksum_list_finish(checksum_list_fast(Sock, Len))}; {ok, <<"ERROR NO-SUCH-FILE", _/binary>>} -> {error, no_such_file}; {ok, <<"ERROR BAD-ARG", _/binary>>} -> @@ -323,6 +326,22 @@ checksum_list_fast(Sock, Remaining) -> {ok, Bytes} = gen_tcp:recv(Sock, Num), [Bytes|checksum_list_fast(Sock, Remaining - byte_size(Bytes))]. +checksum_list_finish(Chunks) -> + Bin = case Chunks of + [X] -> + X; + _ -> + iolist_to_binary(Chunks) + end, + [begin + CSumLen = byte_size(Line) - 16 - 1 - 8 - 1, + <> = Line, + {machi_util:hexstr_to_int(OffsetHex), + machi_util:hexstr_to_int(SizeHex), + machi_util:hexstr_to_bin(CSum)} + end || Line <- re:split(Bin, "\n", [{return, binary}]), + Line /= <<>>]. write_chunk2(Sock, File0, Offset, Chunk0) -> try diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index d51143c..2b840a2 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -28,15 +28,20 @@ -define(FLU, machi_flu1). -define(FLU_C, machi_flu1_client). +setup_test_flu(RegName, TcpPort, DataDir) -> + clean_up_data_dir(DataDir), + + {ok, FLU1} = ?FLU:start_link([{RegName, TcpPort, DataDir}]), + FLU1. + flu_smoke_test() -> Host = "localhost", TcpPort = 32957, DataDir = "./data", Prefix = <<"prefix!">>, BadPrefix = BadFile = "no/good", - clean_up_data_dir(DataDir), - {ok, FLU1} = ?FLU:start_link([{smoke_flu, TcpPort, DataDir}]), + FLU1 = setup_test_flu(smoke_flu, TcpPort, DataDir), try {error, no_such_file} = ?FLU_C:checksum_list(Host, TcpPort, "does-not-exist"), @@ -48,21 +53,24 @@ flu_smoke_test() -> {ok, {Off1,Len1,File1}} = ?FLU_C:append_chunk(Host, TcpPort, Prefix, Chunk1), {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, File1, Off1, Len1), + {ok, [{_,_,_}]} = ?FLU_C:checksum_list(Host, TcpPort, File1), {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, BadPrefix, Chunk1), + {ok, [{_,File1}]} = ?FLU_C:list_files(Host, TcpPort), + Len1 = size(Chunk1), + {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, + File1, Off1*983, Len1), + {error, partial_read} = ?FLU_C:read_chunk(Host, TcpPort, + File1, Off1, Len1*984), Chunk2 = <<"yo yo">>, Len2 = byte_size(Chunk2), Off2 = ?MINIMUM_OFFSET + 77, - File2 = "smoke-file", + File2 = "smoke-prefix", ok = ?FLU_C:write_chunk(Host, TcpPort, File2, Off2, Chunk2), {error, bad_arg} = ?FLU_C:write_chunk(Host, TcpPort, BadFile, Off2, Chunk2), {ok, Chunk2} = ?FLU_C:read_chunk(Host, TcpPort, File2, Off2, Len2), - {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, - File2, Off2*983, Len2), - {error, partial_read} = ?FLU_C:read_chunk(Host, TcpPort, - File2, Off2, Len2*984), {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, "no!!", Off2, Len2), {error, bad_arg} = ?FLU_C:read_chunk(Host, TcpPort, From 76fcd4d931d27326e92210802d5c6edac7c2475f Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Wed, 1 Apr 2015 18:35:10 +0900 Subject: [PATCH 03/17] Move FLU client 'verify checksums' code from prototype/demo-day-hack --- src/machi_admin_util.erl | 37 +++++++++++++++++---- test/machi_admin_util_test.erl | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 test/machi_admin_util_test.erl diff --git a/src/machi_admin_util.erl b/src/machi_admin_util.erl index 6a9bdc7..424c66d 100644 --- a/src/machi_admin_util.erl +++ b/src/machi_admin_util.erl @@ -27,13 +27,38 @@ -include("machi.hrl"). -verify_file_checksums_remote(Sock, File) -> - verify_file_checksums_remote2(Sock, Sock, File). +-define(FLU_C, machi_flu1_client). -verify_file_checksums_remote(_Host, _TcpPort, File) -> - verify_file_checksums_remote2(todo, todo, File). +verify_file_checksums_remote(Sock1, File) when is_port(Sock1) -> + verify_file_checksums_remote2(Sock1, File). + +verify_file_checksums_remote(Host, TcpPort, File) -> + Sock1 = machi_util:connect(Host, TcpPort), + verify_file_checksums_remote2(Sock1, File). %%%%%%%%%%%%%%%%%%%%%%%%%%% -verify_file_checksums_remote2(Sock, Sock, File) -> - todo. +verify_file_checksums_remote2(Sock1, File) -> + try + {ok, Info} = ?FLU_C:checksum_list(Sock1, File), + Res = lists:foldl(verify_chunk_checksum(Sock1, File), [], Info), + {ok, Res} + catch + What:Why -> + {error, {What, Why, erlang:get_stacktrace()}} + end. + +verify_chunk_checksum(Sock1, File) -> + fun({Offset, Size, CSum}, Acc) -> + case ?FLU_C:read_chunk(Sock1, File, Offset, Size) of + {ok, Chunk} -> + CSum2 = machi_util:checksum(Chunk), + if CSum == CSum2 -> + Acc; + true -> + [{Offset, Size, File, CSum, now, CSum2}|Acc] + end; + _Else -> + [{Offset, Size, File, CSum, now, read_failure}|Acc] + end + end. diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl new file mode 100644 index 0000000..19c487b --- /dev/null +++ b/test/machi_admin_util_test.erl @@ -0,0 +1,60 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_admin_util_test). +-compile(export_all). + +-ifdef(TEST). + +-include("machi.hrl"). + +-define(FLU, machi_flu1). +-define(FLU_C, machi_flu1_client). + +verify_file_checksums_remote_test() -> + Host = "localhost", + TcpPort = 32958, + DataDir = "./data", + FLU1 = machi_flu1_test:setup_test_flu(verify1_flu, TcpPort, DataDir), + Sock1 = machi_util:connect(Host, TcpPort), + try + Prefix = <<"verify_prefix">>, + [{ok, _} = ?FLU_C:append_chunk(Sock1, Prefix, <>) || + X <- lists:seq(1,10)], + {ok, [{_FileSize,File}]} = ?FLU_C:list_files(Sock1), + {ok, []} = machi_admin_util:verify_file_checksums_remote( + Host, TcpPort, File), + + Path = DataDir ++ "/" ++ binary_to_list(File), + {ok, FH} = file:open(Path, [read,write]), + {ok, _} = file:position(FH, ?MINIMUM_OFFSET), + ok = file:write(FH, "y"), + ok = file:write(FH, "yo"), + ok = file:write(FH, "yo!"), + ok = file:close(FH), + {ok, [_,_,_]} = machi_admin_util:verify_file_checksums_remote( + Host, TcpPort, File) + after + catch ?FLU_C:quick(Sock1), + ok = ?FLU:stop(FLU1) + end. + +-endif. % TEST + From f8263c15cc2707f5edd3f8ca4bf51932463acda5 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 12:38:12 +0900 Subject: [PATCH 04/17] Move FLU client 'verify checksums + local path' code from prototype/demo-day-hack --- Makefile | 3 ++ src/machi_admin_util.erl | 57 +++++++++++++++++++++++++++++++--- src/machi_flu1_client.erl | 39 +++++++++++++---------- src/machi_util.erl | 7 ++++- test/machi_admin_util_test.erl | 18 ++++++++--- 5 files changed, 98 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index 310b8a8..ba8df11 100644 --- a/Makefile +++ b/Makefile @@ -34,5 +34,8 @@ build_plt: deps compile dialyzer: deps compile dialyzer -Wno_return --plt $(PLT) ebin +dialyzer-test: deps compile + dialyzer -Wno_return --plt $(PLT) .eunit + clean_plt: rm $(PLT) diff --git a/src/machi_admin_util.erl b/src/machi_admin_util.erl index 424c66d..4698e1a 100644 --- a/src/machi_admin_util.erl +++ b/src/machi_admin_util.erl @@ -20,7 +20,13 @@ -module(machi_admin_util). +%% TODO Move these types to a common header file? (also machi_flu1_client.erl?) +-type inet_host() :: inet:ip_address() | inet:hostname(). +-type inet_port() :: inet:port_number(). + -export([ + %% verify_file_checksums_local/2, + verify_file_checksums_local/3, verify_file_checksums_remote/2, verify_file_checksums_remote/3 ]). -compile(export_all). @@ -29,28 +35,69 @@ -define(FLU_C, machi_flu1_client). +-spec verify_file_checksums_local(inet_host(), inet_port(), binary()|list()) -> + {ok, [tuple()]} | {error, term()}. +verify_file_checksums_local(Host, TcpPort, Path) -> + Sock1 = machi_util:connect(Host, TcpPort), + verify_file_checksums_local2(Sock1, Path). + +-spec verify_file_checksums_remote(port(), binary()|list()) -> + {ok, [tuple()]} | {error, term()}. verify_file_checksums_remote(Sock1, File) when is_port(Sock1) -> verify_file_checksums_remote2(Sock1, File). +-spec verify_file_checksums_remote(inet_host(), inet_port(), binary()|list()) -> + {ok, [tuple()]} | {error, term()}. verify_file_checksums_remote(Host, TcpPort, File) -> Sock1 = machi_util:connect(Host, TcpPort), verify_file_checksums_remote2(Sock1, File). %%%%%%%%%%%%%%%%%%%%%%%%%%% +verify_file_checksums_local2(Sock1, Path0) -> + Path = machi_util:make_string(Path0), + case file:open(Path, [read, binary, raw]) of + {ok, FH} -> + File = re:replace(Path, ".*/", "", [{return, binary}]), + try + ReadChunk = fun(_File, Offset, Size) -> + file:pread(FH, Offset, Size) + end, + verify_file_checksums_common(Sock1, File, ReadChunk) + after + file:close(FH) + end; + Else -> + Else + end. + verify_file_checksums_remote2(Sock1, File) -> + ReadChunk = fun(File_name, Offset, Size) -> + ?FLU_C:read_chunk(Sock1, File_name, Offset, Size) + end, + verify_file_checksums_common(Sock1, File, ReadChunk). + +verify_file_checksums_common(Sock1, File, ReadChunk) -> try - {ok, Info} = ?FLU_C:checksum_list(Sock1, File), - Res = lists:foldl(verify_chunk_checksum(Sock1, File), [], Info), - {ok, Res} + case ?FLU_C:checksum_list(Sock1, File) of + {ok, Info} -> + ?FLU_C:checksum_list(Sock1, File), + Res = lists:foldl(verify_chunk_checksum(File, ReadChunk), + [], Info), + {ok, Res}; + {error, no_such_file}=Nope -> + Nope; + {error, _}=Else -> + Else + end catch What:Why -> {error, {What, Why, erlang:get_stacktrace()}} end. -verify_chunk_checksum(Sock1, File) -> +verify_chunk_checksum(File, ReadChunk) -> fun({Offset, Size, CSum}, Acc) -> - case ?FLU_C:read_chunk(Sock1, File, Offset, Size) of + case ReadChunk(File, Offset, Size) of {ok, Chunk} -> CSum2 = machi_util:checksum(Chunk), if CSum == CSum2 -> diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 7e8bc1b..87accc0 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -36,15 +36,18 @@ trunc_hack/2, trunc_hack/3 ]). --type chunk() :: iolist(). --type chunk_s() :: binary(). +-type chunk() :: binary() | iolist(). % client can use either +-type chunk_csum() :: {file_offset(), chunk_size(), binary()}. +-type chunk_s() :: binary(). % server always uses binary() -type chunk_pos() :: {file_offset(), chunk_size(), file_name_s()}. -type chunk_size() :: non_neg_integer(). -type inet_host() :: inet:ip_address() | inet:hostname(). -type inet_port() :: inet:port_number(). +-type file_info() :: {file_size(), file_name_s()}. -type file_name() :: binary() | list(). -type file_name_s() :: binary(). % server reply -type file_offset() :: non_neg_integer(). +-type file_size() :: non_neg_integer(). -type file_prefix() :: binary() | list(). %% @doc Append a chunk (binary- or iolist-style) of data to a file @@ -72,14 +75,16 @@ append_chunk(Host, TcpPort, Prefix, Chunk) -> -spec read_chunk(port(), file_name(), file_offset(), chunk_size()) -> {ok, chunk_s()} | {error, term()}. -read_chunk(Sock, File, Offset, Size) -> +read_chunk(Sock, File, Offset, Size) + when Offset >= ?MINIMUM_OFFSET, Size >= 0 -> read_chunk2(Sock, File, Offset, Size). %% @doc Read a chunk of data of size `Size' from `File' at `Offset'. -spec read_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk_size()) -> {ok, chunk_s()} | {error, term()}. -read_chunk(Host, TcpPort, File, Offset, Size) -> +read_chunk(Host, TcpPort, File, Offset, Size) + when Offset >= ?MINIMUM_OFFSET, Size >= 0 -> Sock = machi_util:connect(Host, TcpPort), try read_chunk2(Sock, File, Offset, Size) @@ -90,14 +95,14 @@ read_chunk(Host, TcpPort, File, Offset, Size) -> %% @doc Fetch the list of chunk checksums for `File'. -spec checksum_list(port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + {ok, [chunk_csum()]} | {error, term()}. checksum_list(Sock, File) when is_port(Sock) -> checksum_list2(Sock, File). %% @doc Fetch the list of chunk checksums for `File'. -spec checksum_list(inet_host(), inet_port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + {ok, [chunk_csum()]} | {error, term()}. checksum_list(Host, TcpPort, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try @@ -109,14 +114,14 @@ checksum_list(Host, TcpPort, File) when is_integer(TcpPort) -> %% @doc Fetch the list of all files on the remote FLU. -spec list_files(port()) -> - {ok, [file_name()]} | {error, term()}. + {ok, [file_info()]} | {error, term()}. list_files(Sock) when is_port(Sock) -> list2(Sock). %% @doc Fetch the list of all files on the remote FLU. -spec list_files(inet_host(), inet_port()) -> - {ok, [file_name()]} | {error, term()}. + {ok, [file_info()]} | {error, term()}. list_files(Host, TcpPort) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try @@ -140,16 +145,18 @@ quit(Sock) when is_port(Sock) -> %% `File' at `Offset'. -spec write_chunk(port(), file_name(), file_offset(), chunk()) -> - {ok, chunk_s()} | {error, term()}. -write_chunk(Sock, File, Offset, Chunk) -> + ok | {error, term()}. +write_chunk(Sock, File, Offset, Chunk) + when Offset >= ?MINIMUM_OFFSET -> write_chunk2(Sock, File, Offset, Chunk). %% @doc Restricted API: Write a chunk of already-sequenced data to %% `File' at `Offset'. -spec write_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk()) -> - {ok, chunk_s()} | {error, term()}. -write_chunk(Host, TcpPort, File, Offset, Chunk) -> + ok | {error, term()}. +write_chunk(Host, TcpPort, File, Offset, Chunk) + when Offset >= ?MINIMUM_OFFSET -> Sock = machi_util:connect(Host, TcpPort), try write_chunk2(Sock, File, Offset, Chunk) @@ -161,7 +168,7 @@ write_chunk(Host, TcpPort, File, Offset, Chunk) -> %% migrated. -spec delete_migration(port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + ok | {error, term()}. delete_migration(Sock, File) when is_port(Sock) -> delete_migration2(Sock, File). @@ -169,7 +176,7 @@ delete_migration(Sock, File) when is_port(Sock) -> %% migrated. -spec delete_migration(inet_host(), inet_port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + ok | {error, term()}. delete_migration(Host, TcpPort, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try @@ -182,7 +189,7 @@ delete_migration(Host, TcpPort, File) when is_integer(TcpPort) -> %% erasure coded. -spec trunc_hack(port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + ok | {error, term()}. trunc_hack(Sock, File) when is_port(Sock) -> trunc_hack2(Sock, File). @@ -190,7 +197,7 @@ trunc_hack(Sock, File) when is_port(Sock) -> %% erasure coded. -spec trunc_hack(inet_host(), inet_port(), file_name()) -> - {ok, [file_name()]} | {error, term()}. + ok | {error, term()}. trunc_hack(Host, TcpPort, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try diff --git a/src/machi_util.erl b/src/machi_util.erl index c859574..25796c3 100644 --- a/src/machi_util.erl +++ b/src/machi_util.erl @@ -24,7 +24,7 @@ checksum/1, hexstr_to_bin/1, bin_to_hexstr/1, hexstr_to_int/1, int_to_hexstr/2, int_to_hexbin/2, - make_binary/1, + make_binary/1, make_string/1, make_regname/1, make_checksum_filename/2, make_data_filename/2, read_max_filenum/2, increment_max_filenum/2, @@ -118,6 +118,11 @@ make_binary(X) when is_binary(X) -> make_binary(X) when is_list(X) -> iolist_to_binary(X). +make_string(X) when is_list(X) -> + lists:flatten(X); +make_string(X) when is_binary(X) -> + binary_to_list(X). + hexstr_to_int(X) -> B = hexstr_to_bin(X), B_size = byte_size(B) * 8, diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl index 19c487b..640e760 100644 --- a/test/machi_admin_util_test.erl +++ b/test/machi_admin_util_test.erl @@ -28,7 +28,7 @@ -define(FLU, machi_flu1). -define(FLU_C, machi_flu1_client). -verify_file_checksums_remote_test() -> +verify_file_checksums_test() -> Host = "localhost", TcpPort = 32958, DataDir = "./data", @@ -49,10 +49,20 @@ verify_file_checksums_remote_test() -> ok = file:write(FH, "yo"), ok = file:write(FH, "yo!"), ok = file:close(FH), - {ok, [_,_,_]} = machi_admin_util:verify_file_checksums_remote( - Host, TcpPort, File) + + %% Check the local flavor of the API + {ok, Res1} = machi_admin_util:verify_file_checksums_local( + Host, TcpPort, Path), + 3 = length(Res1), + + %% Check the remote flavor of the API + {ok, Res2} = machi_admin_util:verify_file_checksums_remote( + Host, TcpPort, File), + 3 = length(Res2), + + ok after - catch ?FLU_C:quick(Sock1), + catch ?FLU_C:quit(Sock1), ok = ?FLU:stop(FLU1) end. From 58fa35a674f3e1fa76bbd085061fb21d9c75fbaf Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 14:17:57 +0900 Subject: [PATCH 05/17] Remove escript-related proof-of-concept stuff from machi_util.erl I'd first thought that having that code there would be a kind of useful reminder: please move me somewhere else. However, there's quite a bit there that's "cluster of clusters" stuff and not appropriate for the current short-term work. --- TODO-shortterm.org | 17 + include/machi_projection.hrl | 27 +- src/machi_util.erl | 960 +---------------------------------- 3 files changed, 45 insertions(+), 959 deletions(-) create mode 100644 TODO-shortterm.org diff --git a/TODO-shortterm.org b/TODO-shortterm.org new file mode 100644 index 0000000..e9967d2 --- /dev/null +++ b/TODO-shortterm.org @@ -0,0 +1,17 @@ +* To Do list + +** Done: remove the escript* stuff from machi_util.erl + +** TODO Add functions to manipulate 1-chain projections + +- Add epoch ID = epoch number + checksum of projection! + +** TODO Change all protocol ops to add epoch ID +** TODO Add projection store to each FLU. +** TODO Add projection wedging logic to each FLU. + +- Add no-wedging state to make testing easier? + +** TODO Move prototype/chain-manager code to "top" of source tree +*** TODO Preserve current test code (leave as-is? tiny changes?) +*** TODO Make chain manager code flexible enough to run "real world" or "sim" diff --git a/include/machi_projection.hrl b/include/machi_projection.hrl index 4b431b6..c790a1d 100644 --- a/include/machi_projection.hrl +++ b/include/machi_projection.hrl @@ -18,16 +18,23 @@ %% %% ------------------------------------------------------------------- +-type m_csum() :: {none | sha1 | sha1_excl_final_20, binary()}. +%% -type m_epoch() :: {m_epoch_n(), m_csum()}. +-type m_epoch_n() :: non_neg_integer(). +-type m_server() :: atom(). +-type timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}. + -record(projection, { - %% hard state - epoch :: non_neg_integer(), - last_epoch :: non_neg_integer(), - float_map, - last_float_map, - %% soft state - migrating :: boolean(), - tree, - last_tree - }). + epoch_number :: m_epoch_n(), + epoch_csum :: m_csum(), + all_members :: [m_server()], + down :: [m_server()], + creation_time :: timestamp(), + author_server :: m_server(), + upi :: [m_server()], + repairing :: [m_server()], + dbg :: list(), %proplist(), is checksummed + dbg2 :: list() %proplist(), is not checksummed + }). -define(SHA_MAX, (1 bsl (20*8))). diff --git a/src/machi_util.erl b/src/machi_util.erl index 25796c3..f526803 100644 --- a/src/machi_util.erl +++ b/src/machi_util.erl @@ -135,402 +135,6 @@ int_to_hexstr(I, I_size) -> int_to_hexbin(I, I_size) -> list_to_binary(int_to_hexstr(I, I_size)). -%%%%%%%%%%%%%%%%% - -%%% escript stuff - -main2(["1file-write-redundant-client"]) -> - io:format("Use: Write a local file to a series of servers.\n"), - io:format("Args: BlockSize Prefix LocalFilePath [silent] [Host Port [Host Port ...]]\n"), - erlang:halt(1); -main2(["1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile|HPs0]) -> - BlockSize = list_to_integer(BlockSizeStr), - Prefix = list_to_binary(PrefixStr), - {Out, HPs} = case HPs0 of - ["silent"|Rest] -> {silent, Rest}; - _ -> {not_silent, HPs0} - end, - Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile), - if Out /= silent -> - print_upload_details(user, Res); - true -> - ok - end, - Res; - -main2(["chunk-read-client"]) -> - io:format("Use: Read a series of chunks for a single server.\n"), - io:format("Args: Host Port LocalChunkDescriptionPath [OutputPath|'console']\n"), - erlang:halt(1); -main2(["chunk-read-client", Host, PortStr, ChunkFileList]) -> - main2(["chunk-read-client", Host, PortStr, ChunkFileList, "console"]); -main2(["chunk-read-client", Host, PortStr, ChunkFileList, OutputPath]) -> - FH = open_output_file(OutputPath), - OutFun = make_outfun(FH), - try - main2(["chunk-read-client2", Host, PortStr, ChunkFileList, OutFun]) - after - (catch file:close(FH)) - end; -main2(["chunk-read-client2", Host, PortStr, ChunkFileList, ProcFun]) -> - Sock = escript_connect(Host, PortStr), - escript_download_chunks(Sock, ChunkFileList, ProcFun); - -main2(["delete-client"]) -> - io:format("Use: Delete a file (NOT FOR GENERAL USE)\n"), - io:format("Args: Host Port File\n"), - erlang:halt(1); -main2(["delete-client", Host, PortStr, File]) -> - Sock = escript_connect(Host, PortStr), - escript_delete(Sock, File); - -%%%% cc flavors %%%% - -main2(["cc-1file-write-redundant-client"]) -> - io:format("Use: Write a local file to a chain via projection.\n"), - io:format("Args: BlockSize Prefix LocalFilePath ProjectionPath\n"), - erlang:halt(1); -main2(["cc-1file-write-redundant-client", BlockSizeStr, PrefixStr, LocalFile, ProjectionPath]) -> - BlockSize = list_to_integer(BlockSizeStr), - Prefix = list_to_binary(PrefixStr), - {_Chain, RawHPs} = calc_chain(write, ProjectionPath, PrefixStr), - HPs = convert_raw_hps(RawHPs), - Res = escript_upload_redundant(HPs, BlockSize, Prefix, LocalFile), - print_upload_details(user, Res), - Res; - -main2(["cc-chunk-read-client"]) -> - io:format("Use: Read a series of chunks from a chain via projection.\n"), - io:format("Args: ProjectionPath ChunkFileList [OutputPath|'console' \\\n\t[ErrorCorrection_ProjectionPath]]\n"), - erlang:halt(1); -main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList]) -> - main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList,"console", - undefined]); -main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath]) -> - main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, - undefined]); -main2(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, - EC_ProjectionPath]) -> - main3(["cc-chunk-read-client", ProjectionPathOrDir, ChunkFileList, OutputPath, - EC_ProjectionPath]). - -main3(["cc-chunk-read-client", - ProjectionPathOrDir, ChunkFileList, OutputPath, EC_ProjectionPath]) -> - P = read_projection_file(ProjectionPathOrDir), - ChainMap = read_chain_map_file(ProjectionPathOrDir), - FH = open_output_file(OutputPath), - ProcFun = make_outfun(FH), - Res = try - escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun, - EC_ProjectionPath) - after - (catch file:close(FH)) - end, - Res. - --spec connect(inet:ip_address() | inet:hostname(), inet:port_number()) -> - port(). -connect(Host, Port) -> - escript_connect(Host, Port). - -escript_connect(Host, PortStr) when is_list(PortStr) -> - Port = list_to_integer(PortStr), - escript_connect(Host, Port); -escript_connect(Host, Port) when is_integer(Port) -> - {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}, {mode,binary}, - {packet, raw}]), - Sock. - -escript_upload_file(Sock, BlockSize, Prefix, File) -> - {ok, FH} = file:open(File, [read, raw, binary]), - try - escript_upload_file2(file:read(FH, BlockSize), FH, - BlockSize, Prefix, Sock, []) - after - file:close(FH) - end. - -escript_upload_file2({ok, Chunk}, FH, BlockSize, Prefix, Sock, Acc) -> - {OffsetHex, LenHex, File} = upload_chunk_append(Sock, Prefix, Chunk), - verb("~s ~s ~s\n", [OffsetHex, LenHex, File]), - <> = hexstr_to_bin(OffsetHex), - <> = hexstr_to_bin(LenHex), - OSF = {Offset, Size, File}, - escript_upload_file2(file:read(FH, BlockSize), FH, BlockSize, Prefix, Sock, - [OSF|Acc]); -escript_upload_file2(eof, _FH, _BlockSize, _Prefix, _Sock, Acc) -> - lists:reverse(Acc). - -upload_chunk_append(Sock, Prefix, Chunk) -> - %% _ = crypto:hash(md5, Chunk), - Len = byte_size(Chunk), - LenHex = list_to_binary(bin_to_hexstr(<>)), - Cmd = <<"A ", LenHex/binary, " ", Prefix/binary, "\n">>, - ok = gen_tcp:send(Sock, [Cmd, Chunk]), - {ok, Line} = gen_tcp:recv(Sock, 0), - PathLen = byte_size(Line) - 3 - 16 - 1 - 1, - <<"OK ", OffsetHex:16/binary, " ", Path:PathLen/binary, _:1/binary>> = Line, - {OffsetHex, LenHex, Path}. - -upload_chunk_write(Sock, Offset, File, Chunk) when is_integer(Offset) -> - OffsetHex = list_to_binary(bin_to_hexstr(<>)), - upload_chunk_write(Sock, OffsetHex, File, Chunk); -upload_chunk_write(Sock, OffsetHex, File, Chunk) when is_binary(OffsetHex) -> - %% _ = crypto:hash(md5, Chunk), - Len = byte_size(Chunk), - LenHex = list_to_binary(bin_to_hexstr(<>)), - Cmd = <<"W-repl ", OffsetHex/binary, " ", - LenHex/binary, " ", File/binary, "\n">>, - ok = gen_tcp:send(Sock, [Cmd, Chunk]), - {ok, Line} = gen_tcp:recv(Sock, 0), - <<"OK\n">> = Line, - {OffsetHex, LenHex, File}. - -escript_upload_redundant([Host, PortStr|HPs], BlockSize, Prefix, LocalFile) -> - Sock = escript_connect(Host, PortStr), - ok = inet:setopts(Sock, [{packet, line}]), - OSFs = try - escript_upload_file(Sock, BlockSize, Prefix, LocalFile) - after - gen_tcp:close(Sock) - end, - escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs). - -escript_upload_redundant2([], _OSFs, _LocalFile, OSFs) -> - OSFs; -escript_upload_redundant2([Host, PortStr|HPs], OSFs, LocalFile, OSFs) -> - Sock = escript_connect(Host, PortStr), - {ok, FH} = file:open(LocalFile, [read, binary, raw]), - try - [begin - {ok, Chunk} = file:read(FH, Size), - _OSF2 = upload_chunk_write(Sock, Offset, File, Chunk) - %% verb("~p: ~p\n", [{Host, PortStr}, OSF2]) - end || {Offset, Size, File} <- OSFs] - after - gen_tcp:close(Sock), - file:close(FH) - end, - escript_upload_redundant2(HPs, OSFs, LocalFile, OSFs). - -escript_download_chunks(Sock, {{{ChunkLine}}}, ProcFun) -> - escript_download_chunk({ok, ChunkLine}, invalid_fd, Sock, ProcFun); -escript_download_chunks(Sock, ChunkFileList, ProcFun) -> - {ok, FH} = file:open(ChunkFileList, [read, raw, binary]), - escript_download_chunk(file:read_line(FH), FH, Sock, ProcFun). - -escript_download_chunk({ok, Line}, FH, Sock, ProcFun) -> - ChunkOrError = escript_cc_download_chunk2(Sock, Line), - ProcFun(ChunkOrError), - [ChunkOrError| - escript_download_chunk((catch file:read_line(FH)), FH, Sock, ProcFun)]; -escript_download_chunk(eof, _FH, _Sock, ProcFun) -> - ProcFun(eof), - []; -escript_download_chunk(_Else, _FH, _Sock, ProcFun) -> - ProcFun(eof), - []. - -escript_cc_download_chunks({{{ChunkLine}}}, P, ChainMap, ProcFun, - EC_ProjectionPath) -> - escript_cc_download_chunk({ok,ChunkLine}, invalid_fd, P, ChainMap, ProcFun, - EC_ProjectionPath); -escript_cc_download_chunks(ChunkFileList, P, ChainMap, ProcFun, - EC_ProjectionPath) -> - {ok, FH} = file:open(ChunkFileList, [read, raw, binary]), - escript_cc_download_chunk(file:read_line(FH), FH, P, ChainMap, ProcFun, - EC_ProjectionPath). - -escript_cc_download_chunk({ok, Line}, FH, P, ChainMap, ProcFun, - EC_ProjectionPath) -> - RestLen = byte_size(Line) - 16 - 1 - 8 - 1 - 1, - <<_Offset:16/binary, " ", _Len:8/binary, " ", Rest:RestLen/binary, "\n">> - = Line, - Prefix = re:replace(Rest, "\\..*", "", [{return, binary}]), - {_Chains, RawHPs} = calc_chain(read, P, ChainMap, Prefix), - Chunk = lists:foldl( - fun(_RawHP, Bin) when is_binary(Bin) -> Bin; - (RawHP, _) -> - [Host, PortStr] = convert_raw_hps([RawHP]), - Sock = get_cached_sock(Host, PortStr), - case escript_cc_download_chunk2(Sock, Line) of - Bin when is_binary(Bin) -> - Bin; - {error, _} = Error -> - Error; - {erasure_encoded, _} = EC_info -> - escript_cc_download_ec_chunk(EC_info, - EC_ProjectionPath) - end - end, undefined, RawHPs), - ProcFun(Chunk), - [Chunk|escript_cc_download_chunk((catch file:read_line(FH)), - FH, P, ChainMap, ProcFun, - EC_ProjectionPath)]; -escript_cc_download_chunk(eof, _FH, _P, _ChainMap, ProcFun, - _EC_ProjectionPath) -> - ProcFun(eof), - []; -escript_cc_download_chunk(Else, _FH, _P, _ChainMap, ProcFun, - _EC_ProjectionPath) -> - ProcFun(Else), - []. - -escript_cc_download_chunk2(Sock, Line) -> - %% Line includes an LF, so we can be lazy. - CmdLF = [<<"R ">>, Line], - ok = gen_tcp:send(Sock, CmdLF), - case gen_tcp:recv(Sock, 3) of - {ok, <<"OK\n">>} -> - {_Offset, Size, _File} = read_hex_size(Line), - {ok, Chunk} = gen_tcp:recv(Sock, Size), - Chunk; - {ok, Else} -> - {ok, OldOpts} = inet:getopts(Sock, [packet]), - ok = inet:setopts(Sock, [{packet, line}]), - {ok, Else2} = gen_tcp:recv(Sock, 0), - ok = inet:setopts(Sock, OldOpts), - case Else of - <<"ERA">> -> - escript_cc_parse_ec_info(Sock, Line, Else2); - _ -> - {error, {Line, <>}} - end - end. - -escript_cc_parse_ec_info(Sock, Line, Else2) -> - ChompLine = chomp(Line), - {Offset, Size, File} = read_hex_size(ChompLine), - <<"SURE ", BodyLenHex:4/binary, " ", StripeWidthHex:16/binary, " ", - OrigFileLenHex:16/binary, " rs_10_4_v1", _/binary>> = Else2, - <> = hexstr_to_bin(BodyLenHex), - {ok, SummaryBody} = gen_tcp:recv(Sock, BodyLen), - - <> = hexstr_to_bin(StripeWidthHex), - <> = hexstr_to_bin(OrigFileLenHex), - NewFileNum = (Offset div StripeWidth) + 1, - NewOffset = Offset rem StripeWidth, - if Offset + Size > OrigFileLen -> - %% Client's request is larger than original file size, derp - {error, bad_offset_and_size}; - NewOffset + Size > StripeWidth -> - %% Client's request straddles a stripe boundary, TODO fix me - {error, todo_TODO_implement_this_with_two_reads_and_then_glue_together}; - true -> - NewOffsetHex = bin_to_hexstr(<>), - LenHex = bin_to_hexstr(<>), - NewSuffix = file_suffix_rs_10_4_v1(NewFileNum), - NewFile = iolist_to_binary([File, NewSuffix]), - NewLine = iolist_to_binary([NewOffsetHex, " ", LenHex, " ", - NewFile, "\n"]), - {erasure_encoded, {Offset, Size, File, NewOffset, NewFile, - NewFileNum, NewLine, SummaryBody}} - end. - -%% TODO: The EC method/version/type stuff here is loosey-goosey -escript_cc_download_ec_chunk(EC_info, undefined) -> - EC_info; -escript_cc_download_ec_chunk({erasure_encoded, - {_Offset, _Size, _File, _NewOffset, NewFile, - NewFileNum, NewLine, SummaryBody}}, - EC_ProjectionPath) -> - {P, ChainMap} = get_cached_projection(EC_ProjectionPath), - %% Remember: we use the whole file name for hashing, not the prefix - {_Chains, RawHPs} = calc_chain(read, P, ChainMap, NewFile), - RawHP = lists:nth(NewFileNum, RawHPs), - [Host, PortStr] = convert_raw_hps([RawHP]), - Sock = get_cached_sock(Host, PortStr), - case escript_cc_download_chunk2(Sock, NewLine) of - Chunk when is_binary(Chunk) -> - Chunk; - {error, _} = Else -> - io:format("TODO: EC chunk get failed:\n\t~s\n", [NewLine]), - io:format("Use this info to reconstruct:\n\t~p\n\n", [SummaryBody]), - Else - end. - -get_cached_projection(EC_ProjectionPath) -> - case get(cached_projection) of - undefined -> - P = read_projection_file(EC_ProjectionPath), - ChainMap = read_chain_map_file(EC_ProjectionPath), - put(cached_projection, {P, ChainMap}), - get_cached_projection(EC_ProjectionPath); - Stuff -> - Stuff - end. - -file_suffix_rs_10_4_v1(1) -> <<"_k01">>; -file_suffix_rs_10_4_v1(2) -> <<"_k02">>; -file_suffix_rs_10_4_v1(3) -> <<"_k03">>; -file_suffix_rs_10_4_v1(4) -> <<"_k04">>; -file_suffix_rs_10_4_v1(5) -> <<"_k05">>; -file_suffix_rs_10_4_v1(6) -> <<"_k06">>; -file_suffix_rs_10_4_v1(7) -> <<"_k07">>; -file_suffix_rs_10_4_v1(8) -> <<"_k08">>; -file_suffix_rs_10_4_v1(9) -> <<"_k09">>; -file_suffix_rs_10_4_v1(10) -> <<"_k10">>. - -escript_delete(Sock, File) -> - ok = gen_tcp:send(Sock, [<<"DEL-migration ">>, File, <<"\n">>]), - ok = inet:setopts(Sock, [{packet, line}]), - case gen_tcp:recv(Sock, 0) of - {ok, <<"OK\n">>} -> - ok; - {ok, <<"ERROR", _/binary>>} -> - error - end. - -escript_compare_servers(Sock1, Sock2, H1, H2, Args) -> - FileFilterFun = fun(_) -> true end, - escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args). - -escript_compare_servers(Sock1, Sock2, H1, H2, FileFilterFun, Args) -> - All = [H1, H2], - put(mydict, dict:new()), - Fetch1 = make_fetcher(H1), - Fetch2 = make_fetcher(H2), - - Fmt = case Args of - [] -> - fun(eof) -> ok; (Str) -> io:format(user, Str, []) end; - [null] -> - fun(_) -> ok end; - [OutFile] -> - {ok, FH} = file:open(OutFile, [write]), - fun(eof) -> file:close(FH); - (Str) -> file:write(FH, Str) - end - end, - - %% TODO: Broken! Fetch1 and Fetch2 aren't created when comments are below - Sock1=Sock1,Sock2=Sock2,Fetch1=Fetch1,Fetch2=Fetch2, % shut up compiler - %% _X1 = escript_list2(Sock1, Fetch1), - %% _X2 = escript_list2(Sock2, Fetch2), - FoldRes = lists:sort(dict:to_list(get(mydict))), - Fmt("{legend, {file, list_of_servers_without_file}}.\n"), - Fmt(io_lib:format("{all, ~p}.\n", [All])), - Res = [begin - {GotIt, Sizes} = lists:unzip(GotSizes), - Size = lists:max(Sizes), - Missing = {File, {Size, All -- GotIt}}, - verb("~p.\n", [Missing]), - Missing - end || {File, GotSizes} <- FoldRes, FileFilterFun(File)], - (catch Fmt(eof)), - Res. - -make_fetcher(Host) -> - fun(eof) -> - ok; - (<>) -> - <> = hexstr_to_bin(SizeHex), - FileLen = byte_size(Rest) - 1, - <> = Rest, - NewDict = dict:append(File, {Host, Size}, get(mydict)), - put(mydict, NewDict) - end. - checksum(Bin) when is_binary(Bin) -> crypto:hash(md5, Bin). @@ -548,560 +152,18 @@ info_msg(Fmt, Args) -> _ -> error_logger:info_msg(Fmt, Args) end. -repair(File, Size, [], Mode, V, SrcS, SrcS2, DstS, DstS2, _Src) -> - verb("~s: present on both: ", [File]), - repair_both_present(File, Size, Mode, V, SrcS, SrcS2, DstS, DstS2); -repair(File, Size, MissingList, Mode, V, SrcS, SrcS2, DstS, _DstS2, Src) -> - case lists:member(Src, MissingList) of - true -> - verb("~s -> ~p, skipping: not on source server\n", [File, MissingList]); - false when Mode == check -> - verb("~s -> ~p, copy ~s MB (skipped)\n", [File, MissingList, mbytes(Size)]); - false -> - verb("~s -> ~p, copy ~s MB ", [File, MissingList, mbytes(Size)]), - ok = copy_file(File, SrcS, SrcS2, DstS, V), - verb("done\n", []) - end. - -copy_file(File, SrcS, SrcS2, DstS, Verbose) -> - %% Use the *second* source socket to copy each chunk. - ProcChecksum = copy_file_proc_checksum_fun(File, SrcS2, DstS, Verbose), - %% Use the *first source socket to enumerate the chunks & checksums. - exit(todo_broken), - machi_flu1_client:checksum_list(SrcS, File, line_by_line, ProcChecksum). - -copy_file_proc_checksum_fun(File, SrcS, DstS, _Verbose) -> - fun(<>) -> - <> = hexstr_to_bin(LenHex), - DownloadChunkBin = <>, - [Chunk] = escript_download_chunks(SrcS, {{{DownloadChunkBin}}}, - fun(_) -> ok end), - CSum = hexstr_to_bin(CSumHex), - CSum2 = checksum(Chunk), - if Len == byte_size(Chunk), CSum == CSum2 -> - {_,_,_} = upload_chunk_write(DstS, OffsetHex, File, Chunk), - ok; - true -> - io:format("ERROR: ~s ~s ~s csum/size error\n", - [File, OffsetHex, LenHex]), - error - end; - (_Else) -> - ok - end. - -repair_both_present(File, Size, Mode, V, SrcS, _SrcS2, DstS, _DstS2) -> - Tmp1 = lists:flatten(io_lib:format("/tmp/sort.1.~w.~w.~w", tuple_to_list(now()))), - Tmp2 = lists:flatten(io_lib:format("/tmp/sort.2.~w.~w.~w", tuple_to_list(now()))), - J_Both = lists:flatten(io_lib:format("/tmp/join.3-both.~w.~w.~w", tuple_to_list(now()))), - J_SrcOnly = lists:flatten(io_lib:format("/tmp/join.4-src-only.~w.~w.~w", tuple_to_list(now()))), - J_DstOnly = lists:flatten(io_lib:format("/tmp/join.5-dst-only.~w.~w.~w", tuple_to_list(now()))), - S_Identical = lists:flatten(io_lib:format("/tmp/join.6-sort-identical.~w.~w.~w", tuple_to_list(now()))), - {ok, FH1} = file:open(Tmp1, [write, raw, binary]), - {ok, FH2} = file:open(Tmp2, [write, raw, binary]), - try - K = md5_ctx, - MD5_it = fun(Bin) -> - {FH, MD5ctx1} = get(K), - file:write(FH, Bin), - MD5ctx2 = crypto:hash_update(MD5ctx1, Bin), - put(K, {FH, MD5ctx2}) - end, - put(K, {FH1, crypto:hash_init(md5)}), - exit(todo_broken), - ok = machi_flu1_client:checksum_list(SrcS, File, fast, MD5_it), - {_, MD5_1} = get(K), - SrcMD5 = crypto:hash_final(MD5_1), - put(K, {FH2, crypto:hash_init(md5)}), - exit(todo_broken), - ok = machi_flu1_client:checksum_list(DstS, File, fast, MD5_it), - {_, MD5_2} = get(K), - DstMD5 = crypto:hash_final(MD5_2), - if SrcMD5 == DstMD5 -> - verb("identical\n", []); - true -> - ok = file:close(FH1), - ok = file:close(FH2), - _Q1 = os:cmd("./REPAIR-SORT-JOIN.sh " ++ Tmp1 ++ " " ++ Tmp2 ++ " " ++ J_Both ++ " " ++ J_SrcOnly ++ " " ++ J_DstOnly ++ " " ++ S_Identical), - case file:read_file_info(S_Identical) of - {ok, _} -> - verb("identical (secondary sort)\n", []); - {error, enoent} -> - io:format("differences found:"), - repair_both(File, Size, V, Mode, - J_Both, J_SrcOnly, J_DstOnly, - SrcS, DstS) - end - end - after - catch file:close(FH1), - catch file:close(FH2), - [(catch file:delete(FF)) || FF <- [Tmp1,Tmp2,J_Both,J_SrcOnly,J_DstOnly, - S_Identical]] - end. - -repair_both(File, _Size, V, Mode, J_Both, J_SrcOnly, J_DstOnly, SrcS, DstS) -> - AccFun = if Mode == check -> - fun(_X, List) -> List end; - Mode == repair -> - fun( X, List) -> [X|List] end - end, - BothFun = fun(<<_OffsetSrcHex:16/binary, " ", - LenSrcHex:8/binary, " ", CSumSrcHex:32/binary, " ", - LenDstHex:8/binary, " ", CSumDstHex:32/binary, "\n">> =Line, - {SameB, SameC, DiffB, DiffC, Ds}) -> - <> = hexstr_to_bin(LenSrcHex), - if LenSrcHex == LenDstHex, - CSumSrcHex == CSumDstHex -> - {SameB + Len, SameC + 1, DiffB, DiffC, Ds}; - true -> - %% D = {OffsetSrcHex, LenSrcHex, ........ - {SameB, SameC, DiffB + Len, DiffC + 1, - AccFun(Line, Ds)} - end; - (_Else, Acc) -> - Acc - end, - OnlyFun = fun(<<_OffsetSrcHex:16/binary, " ", LenSrcHex:8/binary, " ", - _CSumHex:32/binary, "\n">> = Line, - {DiffB, DiffC, Ds}) -> - <> = hexstr_to_bin(LenSrcHex), - {DiffB + Len, DiffC + 1, AccFun(Line, Ds)}; - (_Else, Acc) -> - Acc - end, - {SameBx, SameCx, DiffBy, DiffCy, BothDiffs} = - file_folder(BothFun, {0,0,0,0,[]}, J_Both), - {DiffB_src, DiffC_src, Ds_src} = file_folder(OnlyFun, {0,0,[]}, J_SrcOnly), - {DiffB_dst, DiffC_dst, Ds_dst} = file_folder(OnlyFun, {0,0,[]}, J_DstOnly), - if Mode == check orelse V == true -> - io:format("\n\t"), - io:format("BothR ~p, ", [{SameBx, SameCx, DiffBy, DiffCy}]), - io:format("SrcR ~p, ", [{DiffB_src, DiffC_src}]), - io:format("DstR ~p", [{DiffB_dst, DiffC_dst}]), - io:format("\n"); - true -> ok - end, - if Mode == repair -> - ok = repair_both_both(File, V, BothDiffs, SrcS, DstS), - ok = repair_copy_chunks(File, V, Ds_src, DiffB_src, DiffC_src, - SrcS, DstS), - ok = repair_copy_chunks(File, V, Ds_dst, DiffB_dst, DiffC_dst, - DstS, SrcS); - true -> - ok - end. - -repair_both_both(_File, _V, [_|_], _SrcS, _DstS) -> - %% TODO: fetch both, check checksums, hopefully only exactly one - %% is correct, then use that one to repair the other. And if the - %% sizes are different, hrm, there may be an extra corner case(s) - %% hiding there. - io:format("WHOA! We have differing checksums or sizes here, TODO not implemented, but there's trouble in the little village on the river....\n"), - timer:sleep(3*1000), - ok; -repair_both_both(_File, _V, [], _SrcS, _DstS) -> - ok. - -repair_copy_chunks(_File, _V, [], _DiffBytes, _DiffCount, _SrcS, _DstS) -> - ok; -repair_copy_chunks(File, V, ToBeCopied, DiffBytes, DiffCount, SrcS, DstS) -> - verb("\n", []), - verb("Starting copy of ~p chunks/~s MBytes to \n ~s: ", - [DiffCount, mbytes(DiffBytes), File]), - InnerCopyFun = copy_file_proc_checksum_fun(File, SrcS, DstS, V), - FoldFun = fun(Line, ok) -> - ok = InnerCopyFun(Line) % Strong sanity check - end, - ok = lists:foldl(FoldFun, ok, ToBeCopied), - verb(" done\n", []), - ok. - -file_folder(Fun, Acc, Path) -> - {ok, FH} = file:open(Path, [read, raw, binary]), - try - file_folder2(Fun, Acc, FH) - after - file:close(FH) - end. - -file_folder2(Fun, Acc, FH) -> - file_folder2(file:read_line(FH), Fun, Acc, FH). - -file_folder2({ok, Line}, Fun, Acc, FH) -> - Acc2 = Fun(Line, Acc), - file_folder2(Fun, Acc2, FH); -file_folder2(eof, _Fun, Acc, _FH) -> - Acc. - -make_repair_props(["check"|T]) -> - [{mode, check}|make_repair_props(T)]; -make_repair_props(["repair"|T]) -> - [{mode, repair}|make_repair_props(T)]; -make_repair_props(["verbose"|T]) -> - application:set_env(kernel, verbose, true), - [{verbose, true}|make_repair_props(T)]; -make_repair_props(["noverbose"|T]) -> - [{verbose, false}|make_repair_props(T)]; -make_repair_props(["progress"|T]) -> - [{progress, true}|make_repair_props(T)]; -make_repair_props(["delete-source"|T]) -> - [{delete_source, true}|make_repair_props(T)]; -make_repair_props(["nodelete-source"|T]) -> - [{delete_source, false}|make_repair_props(T)]; -make_repair_props(["nodelete-tmp"|T]) -> - [{delete_tmp, false}|make_repair_props(T)]; -make_repair_props([X|T]) -> - io:format("Error: skipping unknown option ~p\n", [X]), - make_repair_props(T); -make_repair_props([]) -> - %% Proplist defaults - [{mode, check}, {delete_source, false}]. - -mbytes(0) -> - "0.0"; -mbytes(Size) -> - lists:flatten(io_lib:format("~.1.0f", [max(0.1, Size / (1024*1024))])). - -chomp(Line) when is_binary(Line) -> - LineLen = byte_size(Line) - 1, - <> = Line, - ChompLine. - -make_outfun(FH) -> - fun({error, _} = Error) -> - file:write(FH, io_lib:format("Error: ~p\n", [Error])); - (eof) -> - ok; - ({erasure_encoded, Info} = _Erasure) -> - file:write(FH, "TODO/WIP: erasure_coded:\n"), - file:write(FH, io_lib:format("\t~p\n", [Info])); - (Bytes) when is_binary(Bytes) orelse is_list(Bytes) -> - file:write(FH, Bytes) - end. - -open_output_file("console") -> - user; -open_output_file(Path) -> - {ok, FH} = file:open(Path, [write]), - FH. - -print_upload_details(_, {error, _} = Res) -> - io:format("Error: ~p\n", [Res]), - erlang:halt(1); -print_upload_details(FH, Res) -> - [io:format(FH, "~s ~s ~s\n", [bin_to_hexstr(<>), - bin_to_hexstr(<>), - File]) || - {Offset, Len, File} <- Res]. - %%%%%%%%%%%%%%%%% -read_projection_file("new") -> - #projection{epoch=0, last_epoch=0, - float_map=undefined, last_float_map=undefined}; -read_projection_file(Path) -> - case filelib:is_dir(Path) of - true -> - read_projection_file_loop(Path ++ "/current.proj"); - false -> - case filelib:is_file(Path) of - true -> - read_projection_file2(Path); - false -> - error({bummer, Path}) - end - end. +-spec connect(inet:ip_address() | inet:hostname(), inet:port_number()) -> + port(). +connect(Host, Port) -> + escript_connect(Host, Port). -read_projection_file2(Path) -> - {ok, [P]} = file:consult(Path), - true = is_record(P, projection), - FloatMap = P#projection.float_map, - LastFloatMap = if P#projection.last_float_map == undefined -> - FloatMap; - true -> - P#projection.last_float_map - end, - P#projection{migrating=(FloatMap /= LastFloatMap), - tree=machi_chash:make_tree(FloatMap), - last_tree=machi_chash:make_tree(LastFloatMap)}. +escript_connect(Host, PortStr) when is_list(PortStr) -> + Port = list_to_integer(PortStr), + escript_connect(Host, Port); +escript_connect(Host, Port) when is_integer(Port) -> + {ok, Sock} = gen_tcp:connect(Host, Port, [{active,false}, {mode,binary}, + {packet, raw}]), + Sock. -read_projection_file_loop(Path) -> - read_projection_file_loop(Path, 100). - -read_projection_file_loop(Path, 0) -> - error({bummer, Path}); -read_projection_file_loop(Path, N) -> - try - read_projection_file2(Path) - catch - error:{badmatch,{error,enoent}} -> - timer:sleep(100), - read_projection_file_loop(Path, N-1) - end. - -write_projection(P, Path) when is_record(P, projection) -> - {error, enoent} = file:read_file_info(Path), - {ok, FH} = file:open(Path, [write]), - WritingP = P#projection{tree=undefined, last_tree=undefined}, - io:format(FH, "~p.\n", [WritingP]), - ok = file:close(FH). - -read_weight_map_file(Path) -> - {ok, [Map]} = file:consult(Path), - true = is_list(Map), - true = lists:all(fun({Chain, Weight}) - when is_binary(Chain), - is_integer(Weight), Weight >= 0 -> - true; - (_) -> - false - end, Map), - Map. - -%% Assume the file "chains.map" in whatever dir that stores projections. -read_chain_map_file(DirPath) -> - L = case filelib:is_dir(DirPath) of - true -> - {ok, Map} = file:consult(DirPath ++ "/chains.map"), - Map; - false -> - Dir = filename:dirname(DirPath), - {ok, Map} = file:consult(Dir ++ "/chains.map"), - Map - end, - orddict:from_list(L). - -get_float_map(P) when is_record(P, projection) -> - P#projection.float_map. - -get_last_float_map(P) when is_record(P, projection) -> - P#projection.last_float_map. - -hash_and_query(Key, P) when is_record(P, projection) -> - <> = crypto:hash(sha, Key), - Float = Int / ?SHA_MAX, - {_, Current} = machi_chash:query_tree(Float, P#projection.tree), - if P#projection.migrating -> - {_, Last} = machi_chash:query_tree(Float, P#projection.last_tree), - if Last == Current -> - [Current]; - true -> - [Current, Last, Current] - end; - true -> - [Current] - end. - -calc_chain(write=Op, ProjectionPathOrDir, PrefixStr) -> - P = read_projection_file(ProjectionPathOrDir), - ChainMap = read_chain_map_file(ProjectionPathOrDir), - calc_chain(Op, P, ChainMap, PrefixStr); -calc_chain(read=Op, ProjectionPathOrDir, PrefixStr) -> - P = read_projection_file(ProjectionPathOrDir), - ChainMap = read_chain_map_file(ProjectionPathOrDir), - calc_chain(Op, P, ChainMap, PrefixStr). - -calc_chain(write=_Op, P, ChainMap, PrefixStr) -> - %% Writes are easy: always use the new location. - [Chain|_] = hash_and_query(PrefixStr, P), - {Chain, orddict:fetch(Chain, ChainMap)}; -calc_chain(read=_Op, P, ChainMap, PrefixStr) -> - %% Reads are slightly trickier: reverse each chain so tail is tried first. - Chains = hash_and_query(PrefixStr, P), - {Chains, lists:flatten([lists:reverse(orddict:fetch(Chain, ChainMap)) || - Chain <- Chains])}. - -convert_raw_hps([{HostBin, Port}|T]) -> - [binary_to_list(HostBin), integer_to_list(Port)|convert_raw_hps(T)]; -convert_raw_hps([]) -> - []. - -get_cached_sock(Host, PortStr) -> - K = {socket_cache, Host, PortStr}, - case erlang:get(K) of - undefined -> - Sock = escript_connect(Host, PortStr), - Krev = {socket_cache_rev, Sock}, - erlang:put(K, Sock), - erlang:put(Krev, {Host, PortStr}), - Sock; - Sock -> - Sock - end. - -invalidate_cached_sock(Sock) -> - (catch gen_tcp:close(Sock)), - Krev = {socket_cache_rev, Sock}, - case erlang:get(Krev) of - undefined -> - ok; - {Host, PortStr} -> - K = {socket_cache, Host, PortStr}, - erlang:erase(Krev), - erlang:erase(K), - ok - end. - -%%%%%%%%%%%%%%%%% - -%%% basho_bench callbacks - --define(SEQ, ?MODULE). --define(DEFAULT_HOSTIP_LIST, [{{127,0,0,1}, 7071}]). - --record(bb, { - host, - port_str, - %% sock, - proj_check_ticker_started=false, - proj_path, - proj, - chain_map - }). - -new(1 = Id) -> - %% broken: start_append_server(), - case basho_bench_config:get(file0_start_listener, no) of - no -> - ok; - {_Port, _DataDir} -> - exit(todo_broken) - end, - timer:sleep(100), - new_common(Id); -new(Id) -> - new_common(Id). - -new_common(Id) -> - random:seed(now()), - ProjectionPathOrDir = - basho_bench_config:get(file0_projection_path, undefined), - - Servers = basho_bench_config:get(file0_ip_list, ?DEFAULT_HOSTIP_LIST), - NumServers = length(Servers), - {Host, Port} = lists:nth((Id rem NumServers) + 1, Servers), - State0 = #bb{host=Host, port_str=integer_to_list(Port), - proj_path=ProjectionPathOrDir}, - {ok, read_projection_info(State0)}. - -run(null, _KeyGen, _ValueGen, State) -> - {ok, State}; -run(keygen_valuegen_then_null, KeyGen, ValueGen, State) -> - _Prefix = KeyGen(), - _Value = ValueGen(), - {ok, State}; -run(append_local_server, KeyGen, ValueGen, State) -> - Prefix = KeyGen(), - Value = ValueGen(), - {_, _} = ?SEQ:append(?SEQ, Prefix, Value), - {ok, State}; -run(append_remote_server, KeyGen, ValueGen, State) -> - Prefix = KeyGen(), - Value = ValueGen(), - bb_do_write_chunk(Prefix, Value, State#bb.host, State#bb.port_str, State); -run(cc_append_remote_server, KeyGen, ValueGen, State0) -> - State = check_projection_check(State0), - Prefix = KeyGen(), - Value = ValueGen(), - {_Chain, ModHPs} = calc_chain(write, State#bb.proj, State#bb.chain_map, - Prefix), - FoldFun = fun({Host, PortStr}, Acc) -> - case bb_do_write_chunk(Prefix, Value, Host, PortStr, - State) of - {ok, _} -> - Acc + 1; - _ -> - Acc - end - end, - case lists:foldl(FoldFun, 0, ModHPs) of - N when is_integer(N), N > 0 -> - {ok, State}; - 0 -> - {error, oh_some_problem_yo, State} - end; -run(read_raw_line_local, KeyGen, _ValueGen, State) -> - {RawLine, Size, _File} = setup_read_raw_line(KeyGen), - bb_do_read_chunk(RawLine, Size, State#bb.host, State#bb.port_str, State); -run(cc_read_raw_line_local, KeyGen, _ValueGen, State0) -> - State = check_projection_check(State0), - {RawLine, Size, File} = setup_read_raw_line(KeyGen), - Prefix = re:replace(File, "\\..*", "", [{return, binary}]), - {_Chain, ModHPs} = calc_chain(read, State#bb.proj, State#bb.chain_map, - Prefix), - FoldFun = fun(_, {ok, _}=Acc) -> - Acc; - ({Host, PortStr}, _Acc) -> - bb_do_read_chunk(RawLine, Size, Host, PortStr, State) - end, - lists:foldl(FoldFun, undefined, ModHPs). - -bb_do_read_chunk(RawLine, Size, Host, PortStr, State) -> - try - Sock = get_cached_sock(Host, PortStr), - try - ok = gen_tcp:send(Sock, [RawLine, <<"\n">>]), - read_chunk(Sock, Size, State) - catch X2:Y2 -> - invalidate_cached_sock(Sock), - {error, {X2,Y2}, State} - end - catch X:Y -> - {error, {X,Y}, State} - end. - -bb_do_write_chunk(Prefix, Value, Host, PortStr, State) -> - try - Sock = get_cached_sock(Host, PortStr), - try - {_, _, _} = upload_chunk_append(Sock, Prefix, Value), - {ok, State} - catch X2:Y2 -> - invalidate_cached_sock(Sock), - {error, {X2,Y2}, State} - end - catch X:Y -> - {error, {X,Y}, State} - end. - -read_chunk(Sock, Size, State) -> - {ok, <<"OK\n">>} = gen_tcp:recv(Sock, 3), - {ok, _Chunk} = gen_tcp:recv(Sock, Size), - {ok, State}. - -setup_read_raw_line(KeyGen) -> - RawLine = KeyGen(), - <<"R ", Rest/binary>> = RawLine, - {_Offset, Size, File} = read_hex_size(Rest), - {RawLine, Size, File}. - -read_hex_size(Line) -> - <> = Line, - <> = hexstr_to_bin(OffsetHex), - <> = hexstr_to_bin(SizeHex), - {Offset, Size, File}. - -read_projection_info(#bb{proj_path=undefined}=State) -> - State; -read_projection_info(#bb{proj_path=ProjectionPathOrDir}=State) -> - Proj = read_projection_file(ProjectionPathOrDir), - ChainMap = read_chain_map_file(ProjectionPathOrDir), - ModChainMap = - [{Chain, [{binary_to_list(Host), integer_to_list(Port)} || - {Host, Port} <- Members]} || - {Chain, Members} <- ChainMap], - State#bb{proj=Proj, chain_map=ModChainMap}. - -check_projection_check(#bb{proj_check_ticker_started=false} = State) -> - timer:send_interval(5*1000 - random:uniform(500), projection_check), - check_projection_check(State#bb{proj_check_ticker_started=true}); -check_projection_check(#bb{proj_check_ticker_started=true} = State) -> - receive - projection_check -> - read_projection_info(State) - after 0 -> - State - end. From 4c3bd81689e266bad9d886f3f3e93bf03b955f6c Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 16:05:06 +0900 Subject: [PATCH 06/17] Add machi_projection.erl and basic new() test --- TODO-shortterm.org | 5 +- include/machi_projection.hrl | 45 ++++++++----- src/machi_projection.erl | 119 +++++++++++++++++++++++++++++++++ test/machi_projection_test.erl | 77 +++++++++++++++++++++ 4 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 src/machi_projection.erl create mode 100644 test/machi_projection_test.erl diff --git a/TODO-shortterm.org b/TODO-shortterm.org index e9967d2..b1ac6ad 100644 --- a/TODO-shortterm.org +++ b/TODO-shortterm.org @@ -1,10 +1,10 @@ * To Do list ** Done: remove the escript* stuff from machi_util.erl - -** TODO Add functions to manipulate 1-chain projections +** Done: Add functions to manipulate 1-chain projections - Add epoch ID = epoch number + checksum of projection! + Done via compare() func. ** TODO Change all protocol ops to add epoch ID ** TODO Add projection store to each FLU. @@ -15,3 +15,4 @@ ** TODO Move prototype/chain-manager code to "top" of source tree *** TODO Preserve current test code (leave as-is? tiny changes?) *** TODO Make chain manager code flexible enough to run "real world" or "sim" +** TODO Replace registered name use from FLU write/append dispatcher diff --git a/include/machi_projection.hrl b/include/machi_projection.hrl index c790a1d..5f5b11b 100644 --- a/include/machi_projection.hrl +++ b/include/machi_projection.hrl @@ -18,23 +18,34 @@ %% %% ------------------------------------------------------------------- --type m_csum() :: {none | sha1 | sha1_excl_final_20, binary()}. -%% -type m_epoch() :: {m_epoch_n(), m_csum()}. --type m_epoch_n() :: non_neg_integer(). --type m_server() :: atom(). --type timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}. +-type pv1_csum() :: binary(). +-type pv1_epoch() :: {pv1_epoch_n(), pv1_csum()}. +-type pv1_epoch_n() :: non_neg_integer(). +-type pv1_server() :: atom() | binary(). +-type pv1_timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}. --record(projection, { - epoch_number :: m_epoch_n(), - epoch_csum :: m_csum(), - all_members :: [m_server()], - down :: [m_server()], - creation_time :: timestamp(), - author_server :: m_server(), - upi :: [m_server()], - repairing :: [m_server()], - dbg :: list(), %proplist(), is checksummed - dbg2 :: list() %proplist(), is not checksummed - }). +-record(projection_v1, { + epoch_number :: pv1_epoch_n(), + epoch_csum :: pv1_csum(), + all_members :: [pv1_server()], + member_dict :: orddict:orddict(), + down :: [pv1_server()], + creation_time :: pv1_timestamp(), + author_server :: pv1_server(), + upi :: [pv1_server()], + repairing :: [pv1_server()], + dbg :: list(), %proplist(), is checksummed + dbg2 :: list() %proplist(), is not checksummed + }). + +-define(MACHI_DEFAULT_TCP_PORT, 50000). + +-record(p_srvr, { + name :: pv1_server(), + proto = 'ipv4' :: 'ipv4' | 'disterl', % disterl? Hrm. + address :: term(), % Protocol-specific + port :: term(), % Protocol-specific + props = [] :: list() % proplist for other related info + }). -define(SHA_MAX, (1 bsl (20*8))). diff --git a/src/machi_projection.erl b/src/machi_projection.erl new file mode 100644 index 0000000..d4f7e42 --- /dev/null +++ b/src/machi_projection.erl @@ -0,0 +1,119 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_projection). + +-include("machi_projection.hrl"). + +-export([ + new/6, new/7, new/8, + update_projection_checksum/1, + update_projection_dbg2/2, + compare/2, + make_projection_summary/1 + ]). + +new(MyName, All_list, UPI_list, Down_list, Repairing_list, Ps) -> + new(0, MyName, All_list, Down_list, UPI_list, Repairing_list, Ps). + +new(EpochNum, MyName, All_list, Down_list, UPI_list, Repairing_list, Dbg) -> + new(EpochNum, MyName, All_list, Down_list, UPI_list, Repairing_list, + Dbg, []). + +new(EpochNum, MyName, All_list0, Down_list, UPI_list, Repairing_list, + Dbg, Dbg2) + when is_integer(EpochNum), EpochNum >= 0, + is_atom(MyName) orelse is_binary(MyName), + is_list(All_list0), is_list(Down_list), is_list(UPI_list), + is_list(Repairing_list), is_list(Dbg), is_list(Dbg2) -> + {All_list, MemberDict} = + case lists:all(fun(P) when is_record(P, p_srvr) -> true; + (_) -> false + end, All_list0) of + true -> + All = [S#p_srvr.name || S <- All_list0], + TmpL = [{S#p_srvr.name, S} || S <- All_list0], + {All, orddict:from_list(TmpL)}; + false -> + All_list1 = lists:zip(All_list0,lists:seq(0,length(All_list0)-1)), + All_list2 = [#p_srvr{name=S, address="localhost", + port=?MACHI_DEFAULT_TCP_PORT+I} || + {S, I} <- All_list1], + TmpL = [{S#p_srvr.name, S} || S <- All_list2], + {All_list0, orddict:from_list(TmpL)} + end, + true = lists:all(fun(X) when is_atom(X) orelse is_binary(X) -> true; + (_) -> false + end, All_list), + [true = lists:sort(SomeList) == lists:usort(SomeList) || + SomeList <- [All_list, Down_list, UPI_list, Repairing_list] ], + AllSet = ordsets:from_list(All_list), + DownSet = ordsets:from_list(Down_list), + UPISet = ordsets:from_list(UPI_list), + RepairingSet = ordsets:from_list(Repairing_list), + + true = ordsets:is_element(MyName, AllSet), + true = (AllSet == ordsets:union([DownSet, UPISet, RepairingSet])), + true = ordsets:is_disjoint(DownSet, UPISet), + true = ordsets:is_disjoint(DownSet, RepairingSet), + true = ordsets:is_disjoint(UPISet, RepairingSet), + + P = #projection_v1{epoch_number=EpochNum, + creation_time=now(), + author_server=MyName, + all_members=All_list, + member_dict=MemberDict, + down=Down_list, + upi=UPI_list, + repairing=Repairing_list, + dbg=Dbg + }, + update_projection_dbg2(update_projection_checksum(P), Dbg2). + +update_projection_checksum(P) -> + CSum = crypto:hash(sha, + term_to_binary(P#projection_v1{epoch_csum= <<>>, + dbg2=[]})), + P#projection_v1{epoch_csum=CSum}. + +update_projection_dbg2(P, Dbg2) when is_list(Dbg2) -> + P#projection_v1{dbg2=Dbg2}. + +-spec compare(#projection_v1{}, #projection_v1{}) -> + integer(). +compare(#projection_v1{epoch_number=E1, epoch_csum=C1}, + #projection_v1{epoch_number=E1, epoch_csum=C1}) -> + 0; +compare(#projection_v1{epoch_number=E1}, + #projection_v1{epoch_number=E2}) -> + if E1 =< E2 -> -1; + E1 > E2 -> 1 + end. + +make_projection_summary(#projection_v1{epoch_number=EpochNum, + all_members=_All_list, + down=Down_list, + author_server=Author, + upi=UPI_list, + repairing=Repairing_list, + dbg=Dbg, dbg2=Dbg2}) -> + [{epoch,EpochNum},{author,Author}, + {upi,UPI_list},{repair,Repairing_list},{down,Down_list}, + {d,Dbg}, {d2,Dbg2}]. diff --git a/test/machi_projection_test.erl b/test/machi_projection_test.erl new file mode 100644 index 0000000..f30411a --- /dev/null +++ b/test/machi_projection_test.erl @@ -0,0 +1,77 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_projection_test). + +-ifdef(TEST). +-compile(export_all). + +-include("machi_projection.hrl"). + +new_test() -> + %% Bleh, hey QuickCheck ... except that any model probably equals + %% code under test, bleh. + true = try_it(a, [a,b,c], [a,b], [], [c], []), + true = try_it(<<"a">>, [<<"a">>,b,c], [<<"a">>,b], [], [c], []), + Servers = [#p_srvr{name=a}, #p_srvr{name=b}, #p_srvr{name=c}], + Servers_bad1 = [#p_srvr{name= <<"a">>}, #p_srvr{name=b}, #p_srvr{name=c}], + Servers_bad2 = [#p_srvr{name=z}, #p_srvr{name=b}, #p_srvr{name=c}], + true = try_it(a, Servers, [a,b], [], [c], []), + + false = try_it(a, not_list, [a,b], [], [c], []), + false = try_it(a, [a,b,c], not_list, [], [c], []), + false = try_it(a, [a,b,c], [a,b], not_list, [c], []), + false = try_it(a, [a,b,c], [a,b], [], not_list, []), + false = try_it(a, [a,b,c], [a,b], [], [c], not_list), + + false = try_it(<<"x">>, [a,b,c], [a,b], [], [c], []), + false = try_it(a, [a,b,c], [a,b,c], [], [c], []), + false = try_it(a, [a,b,c], [a,b], [c], [c], []), + false = try_it(a, [a,b,c], [a,b], [], [c,c], []), + false = try_it(a, Servers_bad1, [a,b], [], [c], []), + false = try_it(a, Servers_bad2, [a,b], [], [c], []), + + ok. + +compare_test() -> + P0 = machi_projection:new(0, a, [a,b,c], [a,b], [], [c], []), + P1a = machi_projection:new(1, a, [a,b,c], [a,b], [], [c], []), + P1b = machi_projection:new(1, b, [a,b,c], [a,b], [], [c], []), + P2 = machi_projection:new(2, a, [a,b,c], [a,b], [], [c], []), + + 0 = machi_projection:compare(P0, P0), + -1 = machi_projection:compare(P0, P1a), + -1 = machi_projection:compare(P1a, P1b), + -1 = machi_projection:compare(P1b, P1a), + 1 = machi_projection:compare(P2, P1a), + 1 = machi_projection:compare(P2, P1b), + 1 = machi_projection:compare(P2, P0), + ok. + +try_it(MyName, All_list, UPI_list, Down_list, Repairing_list, Ps) -> + try + P = machi_projection:new(MyName, All_list, UPI_list, Down_list, + Repairing_list, Ps), + is_record(P, projection_v1) + catch _:_ -> + false + end. + +-endif. % TEST From 5580098d492b404b630fe1f75db2736edbff5066 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 17:16:15 +0900 Subject: [PATCH 07/17] Refactor to use record for FLU state, add dbg mode --- src/machi_flu1.erl | 62 +++++++++++++++++++++++++++------------- test/machi_flu1_test.erl | 6 +++- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 5a5f04e..66ac73b 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -26,9 +26,19 @@ -export([start_link/1, stop/1]). -start_link([{FluName, TcpPort, DataDir}]) +-record(state, { + reg_name :: atom(), + tcp_port :: non_neg_integer(), + data_dir :: string(), + wedge = true :: 'disabled' | boolean(), + my_epoch_id :: 'undefined', + dbg_props = [] :: list(), % proplist + props = [] :: list() % proplist + }). + +start_link([{FluName, TcpPort, DataDir}|Rest]) when is_atom(FluName), is_integer(TcpPort), is_list(DataDir) -> - {ok, spawn_link(fun() -> main2(FluName, TcpPort, DataDir) end)}. + {ok, spawn_link(fun() -> main2(FluName, TcpPort, DataDir, Rest) end)}. stop(Pid) -> case erlang:is_process_alive(Pid) of @@ -41,41 +51,53 @@ stop(Pid) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%% -main2(RegName, TcpPort, DataDir) -> - _Pid1 = start_listen_server(RegName, TcpPort, DataDir), - _Pid2 = start_append_server(RegName, DataDir), +main2(RegName, TcpPort, DataDir, Rest) -> + S1 = #state{reg_name=RegName, + tcp_port=TcpPort, + data_dir=DataDir, + props=Rest}, + S2 = case proplists:get_value(dbg, Rest) of + undefined -> + S1; + DbgProps -> + S1#state{wedge=disabled, + dbg_props=DbgProps, + props=lists:keydelete(dbg, 1, Rest)} + end, + _Pid1 = start_listen_server(S2), + _Pid2 = start_append_server(S2), receive forever -> ok end. -start_listen_server(RegName, TcpPort, DataDir) -> - spawn_link(fun() -> run_listen_server(RegName, TcpPort, DataDir) end). +start_listen_server(S) -> + spawn_link(fun() -> run_listen_server(S) end). -start_append_server(Name, DataDir) -> - spawn_link(fun() -> run_append_server(Name, DataDir) end). +start_append_server(S) -> + spawn_link(fun() -> run_append_server(S) end). -run_listen_server(RegName, TcpPort, DataDir) -> +run_listen_server(#state{tcp_port=TcpPort}=S) -> SockOpts = [{reuseaddr, true}, {mode, binary}, {active, false}, {packet, line}], {ok, LSock} = gen_tcp:listen(TcpPort, SockOpts), - listen_server_loop(RegName, LSock, DataDir). + listen_server_loop(LSock, S). -run_append_server(Name, DataDir) -> +run_append_server(#state{reg_name=Name}=S) -> register(Name, self()), - append_server_loop(DataDir). + append_server_loop(S). -listen_server_loop(RegName, LSock, DataDir) -> +listen_server_loop(LSock, S) -> {ok, Sock} = gen_tcp:accept(LSock), - spawn(fun() -> net_server_loop(RegName, Sock, DataDir) end), - listen_server_loop(RegName, LSock, DataDir). + spawn(fun() -> net_server_loop(Sock, S) end), + listen_server_loop(LSock, S). -append_server_loop(DataDir) -> +append_server_loop(#state{data_dir=DataDir}=S) -> receive {seq_append, From, Prefix, Chunk, CSum} -> spawn(fun() -> append_server_dispatch(From, Prefix, Chunk, CSum, DataDir) end), - append_server_loop(DataDir) + append_server_loop(S) end. -net_server_loop(RegName, Sock, DataDir) -> +net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> ok = inet:setopts(Sock, [{packet, line}]), case gen_tcp:recv(Sock, 0, 60*1000) of {ok, Line} -> @@ -132,7 +154,7 @@ net_server_loop(RegName, Sock, DataDir) -> catch gen_tcp:close(Sock), exit(normal) end, - net_server_loop(RegName, Sock, DataDir); + net_server_loop(Sock, S); _ -> catch gen_tcp:close(Sock), exit(normal) diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 2b840a2..ef960b7 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -29,9 +29,13 @@ -define(FLU_C, machi_flu1_client). setup_test_flu(RegName, TcpPort, DataDir) -> + setup_test_flu(RegName, TcpPort, DataDir, []). + +setup_test_flu(RegName, TcpPort, DataDir, DbgProps) -> clean_up_data_dir(DataDir), - {ok, FLU1} = ?FLU:start_link([{RegName, TcpPort, DataDir}]), + {ok, FLU1} = ?FLU:start_link([{RegName, TcpPort, DataDir}, + {dbg, DbgProps}]), FLU1. flu_smoke_test() -> From 030d2ecd10eef6937b7bf81cd52682936f2726f2 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 17:42:26 +0900 Subject: [PATCH 08/17] Update TODO-shortterm.org + minor stuff --- TODO-shortterm.org | 2 ++ src/machi_flu1.erl | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/TODO-shortterm.org b/TODO-shortterm.org index b1ac6ad..d695d93 100644 --- a/TODO-shortterm.org +++ b/TODO-shortterm.org @@ -7,7 +7,9 @@ Done via compare() func. ** TODO Change all protocol ops to add epoch ID +** TODO Move the FLU server to gen_server behavior? ** TODO Add projection store to each FLU. +** TODO Change all protocol ops to enforce the epoch ID ** TODO Add projection wedging logic to each FLU. - Add no-wedging state to make testing easier? diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 66ac73b..7ad649f 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -64,8 +64,11 @@ main2(RegName, TcpPort, DataDir, Rest) -> dbg_props=DbgProps, props=lists:keydelete(dbg, 1, Rest)} end, - _Pid1 = start_listen_server(S2), - _Pid2 = start_append_server(S2), + AppendPid = start_append_server(S2), + ListenPid = start_listen_server(S2), + put(flu_reg_name, RegName), + put(flu_append_pid, AppendPid), + put(flu_listen_pid, ListenPid), receive forever -> ok end. start_listen_server(S) -> @@ -86,7 +89,7 @@ run_append_server(#state{reg_name=Name}=S) -> listen_server_loop(LSock, S) -> {ok, Sock} = gen_tcp:accept(LSock), - spawn(fun() -> net_server_loop(Sock, S) end), + spawn_link(fun() -> net_server_loop(Sock, S) end), listen_server_loop(LSock, S). append_server_loop(#state{data_dir=DataDir}=S) -> From 44bb5e1dae796ecf9ec5bd929726b9e39b4b4d73 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 18:08:42 +0900 Subject: [PATCH 09/17] WIP: epoch ID added to append protocol command --- include/machi_projection.hrl | 2 ++ src/machi_flu1.erl | 10 ++++++++++ src/machi_flu1_client.erl | 24 +++++++++++++++--------- test/machi_admin_util_test.erl | 4 +++- test/machi_flu1_test.erl | 3 +++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/include/machi_projection.hrl b/include/machi_projection.hrl index 5f5b11b..2e35aed 100644 --- a/include/machi_projection.hrl +++ b/include/machi_projection.hrl @@ -24,6 +24,8 @@ -type pv1_server() :: atom() | binary(). -type pv1_timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}. +-define(DUMMY_PV1_EPOCH, {0,<<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>}). + -record(projection_v1, { epoch_number :: pv1_epoch_n(), epoch_csum :: pv1_csum(), diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 7ad649f..5a8ba7e 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -100,12 +100,15 @@ append_server_loop(#state{data_dir=DataDir}=S) -> append_server_loop(S) end. +-define(EpochIDSpace, (4+20)). + net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> ok = inet:setopts(Sock, [{packet, line}]), case gen_tcp:recv(Sock, 0, 60*1000) of {ok, Line} -> %% machi_util:verb("Got: ~p\n", [Line]), PrefixLenLF = byte_size(Line) - 2 - 8 - 1 - 1, + PrefixLenLF_E = byte_size(Line) - 2 - ?EpochIDSpace - 8 - 1, PrefixLenCRLF = byte_size(Line) - 2 - 8 - 1 - 2, FileLenLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 1, FileLenCRLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 2, @@ -118,6 +121,13 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> <<"A ", LenHex:8/binary, " ", Prefix:PrefixLenLF/binary, "\n">> -> do_net_server_append(RegName, Sock, LenHex, Prefix); +%% BEGIN epoch-id hack + <<"A ", + _EpochIDRaw:(?EpochIDSpace)/binary, + LenHex:8/binary, + Prefix:PrefixLenLF_E/binary, "\n">> -> + do_net_server_append(RegName, Sock, LenHex, Prefix); +%% END epoch-id hack <<"A ", LenHex:8/binary, " ", Prefix:PrefixLenCRLF/binary, "\r\n">> -> do_net_server_append(RegName, Sock, LenHex, Prefix); diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 87accc0..1bbc06d 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -23,7 +23,7 @@ -include("machi.hrl"). -export([ - append_chunk/3, append_chunk/4, + append_chunk/4, append_chunk/5, read_chunk/4, read_chunk/5, checksum_list/2, checksum_list/3, list_files/1, list_files/2, @@ -41,6 +41,9 @@ -type chunk_s() :: binary(). % server always uses binary() -type chunk_pos() :: {file_offset(), chunk_size(), file_name_s()}. -type chunk_size() :: non_neg_integer(). +-type epoch_csum() :: binary(). +-type epoch_num() :: non_neg_integer(). +-type epoch_id() :: {epoch_num(), epoch_csum()}. -type inet_host() :: inet:ip_address() | inet:hostname(). -type inet_port() :: inet:port_number(). -type file_info() :: {file_size(), file_name_s()}. @@ -53,20 +56,21 @@ %% @doc Append a chunk (binary- or iolist-style) of data to a file %% with `Prefix'. --spec append_chunk(port(), file_prefix(), chunk()) -> +-spec append_chunk(port(), epoch_id(), file_prefix(), chunk()) -> {ok, chunk_pos()} | {error, term()}. -append_chunk(Sock, Prefix, Chunk) -> - append_chunk2(Sock, Prefix, Chunk). +append_chunk(Sock, EpochID, Prefix, Chunk) -> + append_chunk2(Sock, EpochID, Prefix, Chunk). %% @doc Append a chunk (binary- or iolist-style) of data to a file %% with `Prefix'. --spec append_chunk(inet_host(), inet_port(), file_prefix(), chunk()) -> +-spec append_chunk(inet_host(), inet_port(), + epoch_id(), file_prefix(), chunk()) -> {ok, chunk_pos()} | {error, term()}. -append_chunk(Host, TcpPort, Prefix, Chunk) -> +append_chunk(Host, TcpPort, EpochID, Prefix, Chunk) -> Sock = machi_util:connect(Host, TcpPort), try - append_chunk2(Sock, Prefix, Chunk) + append_chunk2(Sock, EpochID, Prefix, Chunk) after catch gen_tcp:close(Sock) end. @@ -208,7 +212,7 @@ trunc_hack(Host, TcpPort, File) when is_integer(TcpPort) -> %%%%%%%%%%%%%%%%%%%%%%%%%%% -append_chunk2(Sock, Prefix0, Chunk0) -> +append_chunk2(Sock, EpochID, Prefix0, Chunk0) -> try %% TODO: add client-side checksum to the server's protocol %% _ = crypto:hash(md5, Chunk), @@ -216,8 +220,10 @@ append_chunk2(Sock, Prefix0, Chunk0) -> Chunk = machi_util:make_binary(Chunk0), Len = iolist_size(Chunk0), true = (Len =< ?MAX_CHUNK_SIZE), + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, LenHex = machi_util:int_to_hexbin(Len, 32), - Cmd = <<"A ", LenHex/binary, " ", Prefix/binary, "\n">>, + Cmd = [<<"A ">>, EpochIDRaw, LenHex, Prefix, 10], ok = gen_tcp:send(Sock, [Cmd, Chunk]), {ok, Line} = gen_tcp:recv(Sock, 0), PathLen = byte_size(Line) - 3 - 16 - 1 - 1, diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl index 640e760..dd46af2 100644 --- a/test/machi_admin_util_test.erl +++ b/test/machi_admin_util_test.erl @@ -24,6 +24,7 @@ -ifdef(TEST). -include("machi.hrl"). +-include("machi_projection.hrl"). -define(FLU, machi_flu1). -define(FLU_C, machi_flu1_client). @@ -36,7 +37,8 @@ verify_file_checksums_test() -> Sock1 = machi_util:connect(Host, TcpPort), try Prefix = <<"verify_prefix">>, - [{ok, _} = ?FLU_C:append_chunk(Sock1, Prefix, <>) || + [{ok, _} = ?FLU_C:append_chunk(Sock1, ?DUMMY_PV1_EPOCH, + Prefix, <>) || X <- lists:seq(1,10)], {ok, [{_FileSize,File}]} = ?FLU_C:list_files(Sock1), {ok, []} = machi_admin_util:verify_file_checksums_remote( diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index ef960b7..e006adc 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -23,6 +23,7 @@ -ifdef(TEST). -include("machi.hrl"). +-include("machi_projection.hrl"). -include_lib("eunit/include/eunit.hrl"). -define(FLU, machi_flu1). @@ -55,10 +56,12 @@ flu_smoke_test() -> Chunk1 = <<"yo!">>, {ok, {Off1,Len1,File1}} = ?FLU_C:append_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, Prefix, Chunk1), {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, File1, Off1, Len1), {ok, [{_,_,_}]} = ?FLU_C:checksum_list(Host, TcpPort, File1), {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, BadPrefix, Chunk1), {ok, [{_,File1}]} = ?FLU_C:list_files(Host, TcpPort), Len1 = size(Chunk1), From 9479baac4676e7110298abd83f3269c7efcd5dd4 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 20:31:10 +0900 Subject: [PATCH 10/17] WIP: epoch ID added to read protocol command --- src/machi_admin_util.erl | 4 +++- src/machi_flu1.erl | 22 +++++----------------- src/machi_flu1_client.erl | 21 ++++++++++++--------- test/machi_flu1_test.erl | 10 ++++++++-- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/machi_admin_util.erl b/src/machi_admin_util.erl index 4698e1a..ea0f3bf 100644 --- a/src/machi_admin_util.erl +++ b/src/machi_admin_util.erl @@ -32,6 +32,7 @@ -compile(export_all). -include("machi.hrl"). +-include("machi_projection.hrl"). -define(FLU_C, machi_flu1_client). @@ -73,7 +74,8 @@ verify_file_checksums_local2(Sock1, Path0) -> verify_file_checksums_remote2(Sock1, File) -> ReadChunk = fun(File_name, Offset, Size) -> - ?FLU_C:read_chunk(Sock1, File_name, Offset, Size) + ?FLU_C:read_chunk(Sock1, ?DUMMY_PV1_EPOCH, + File_name, Offset, Size) end, verify_file_checksums_common(Sock1, File, ReadChunk). diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 5a8ba7e..6bea7ec 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -107,35 +107,23 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> case gen_tcp:recv(Sock, 0, 60*1000) of {ok, Line} -> %% machi_util:verb("Got: ~p\n", [Line]), - PrefixLenLF = byte_size(Line) - 2 - 8 - 1 - 1, PrefixLenLF_E = byte_size(Line) - 2 - ?EpochIDSpace - 8 - 1, - PrefixLenCRLF = byte_size(Line) - 2 - 8 - 1 - 2, - FileLenLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 1, - FileLenCRLF = byte_size(Line) - 2 - 16 - 1 - 8 - 1 - 2, + FileLenLF_E = byte_size(Line) - 2 - ?EpochIDSpace - 16 - 8 - 1, CSumFileLenLF = byte_size(Line) - 2 - 1, CSumFileLenCRLF = byte_size(Line) - 2 - 2, WriteFileLenLF = byte_size(Line) - 7 - 16 - 1 - 8 - 1 - 1, DelFileLenLF = byte_size(Line) - 14 - 1, case Line of %% For normal use - <<"A ", LenHex:8/binary, " ", - Prefix:PrefixLenLF/binary, "\n">> -> - do_net_server_append(RegName, Sock, LenHex, Prefix); -%% BEGIN epoch-id hack <<"A ", _EpochIDRaw:(?EpochIDSpace)/binary, LenHex:8/binary, Prefix:PrefixLenLF_E/binary, "\n">> -> do_net_server_append(RegName, Sock, LenHex, Prefix); -%% END epoch-id hack - <<"A ", LenHex:8/binary, " ", - Prefix:PrefixLenCRLF/binary, "\r\n">> -> - do_net_server_append(RegName, Sock, LenHex, Prefix); - <<"R ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", - File:FileLenLF/binary, "\n">> -> - do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); - <<"R ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", - File:FileLenCRLF/binary, "\r\n">> -> + <<"R ", + _EpochIDRaw:(?EpochIDSpace)/binary, + OffsetHex:16/binary, LenHex:8/binary, + File:FileLenLF_E/binary, "\n">> -> do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); <<"L\n">> -> do_net_server_listing(Sock, DataDir); diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 1bbc06d..331b0d1 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -24,7 +24,7 @@ -export([ append_chunk/4, append_chunk/5, - read_chunk/4, read_chunk/5, + read_chunk/5, read_chunk/6, checksum_list/2, checksum_list/3, list_files/1, list_files/2, quit/1 @@ -77,21 +77,22 @@ append_chunk(Host, TcpPort, EpochID, Prefix, Chunk) -> %% @doc Read a chunk of data of size `Size' from `File' at `Offset'. --spec read_chunk(port(), file_name(), file_offset(), chunk_size()) -> +-spec read_chunk(port(), epoch_id(), file_name(), file_offset(), chunk_size()) -> {ok, chunk_s()} | {error, term()}. -read_chunk(Sock, File, Offset, Size) +read_chunk(Sock, EpochID, File, Offset, Size) when Offset >= ?MINIMUM_OFFSET, Size >= 0 -> - read_chunk2(Sock, File, Offset, Size). + read_chunk2(Sock, EpochID, File, Offset, Size). %% @doc Read a chunk of data of size `Size' from `File' at `Offset'. --spec read_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk_size()) -> +-spec read_chunk(inet_host(), inet_port(), epoch_id(), + file_name(), file_offset(), chunk_size()) -> {ok, chunk_s()} | {error, term()}. -read_chunk(Host, TcpPort, File, Offset, Size) +read_chunk(Host, TcpPort, EpochID, File, Offset, Size) when Offset >= ?MINIMUM_OFFSET, Size >= 0 -> Sock = machi_util:connect(Host, TcpPort), try - read_chunk2(Sock, File, Offset, Size) + read_chunk2(Sock, EpochID, File, Offset, Size) after catch gen_tcp:close(Sock) end. @@ -244,11 +245,13 @@ append_chunk2(Sock, EpochID, Prefix0, Chunk0) -> {error, {badmatch, BadMatch, erlang:get_stacktrace()}} end. -read_chunk2(Sock, File0, Offset, Size) -> +read_chunk2(Sock, EpochID, File0, Offset, Size) -> + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, File = machi_util:make_binary(File0), PrefixHex = machi_util:int_to_hexbin(Offset, 64), SizeHex = machi_util:int_to_hexbin(Size, 32), - CmdLF = [$R, 32, PrefixHex, 32, SizeHex, 32, File, 10], + CmdLF = [$R, 32, EpochIDRaw, PrefixHex, SizeHex, File, 10], ok = gen_tcp:send(Sock, CmdLF), case gen_tcp:recv(Sock, 3) of {ok, <<"OK\n">>} -> diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index e006adc..e23f33d 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -58,7 +58,8 @@ flu_smoke_test() -> {ok, {Off1,Len1,File1}} = ?FLU_C:append_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, Prefix, Chunk1), - {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, File1, Off1, Len1), + {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, + File1, Off1, Len1), {ok, [{_,_,_}]} = ?FLU_C:checksum_list(Host, TcpPort, File1), {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, @@ -66,8 +67,10 @@ flu_smoke_test() -> {ok, [{_,File1}]} = ?FLU_C:list_files(Host, TcpPort), Len1 = size(Chunk1), {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, File1, Off1*983, Len1), {error, partial_read} = ?FLU_C:read_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, File1, Off1, Len1*984), Chunk2 = <<"yo yo">>, @@ -77,10 +80,13 @@ flu_smoke_test() -> ok = ?FLU_C:write_chunk(Host, TcpPort, File2, Off2, Chunk2), {error, bad_arg} = ?FLU_C:write_chunk(Host, TcpPort, BadFile, Off2, Chunk2), - {ok, Chunk2} = ?FLU_C:read_chunk(Host, TcpPort, File2, Off2, Len2), + {ok, Chunk2} = ?FLU_C:read_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, + File2, Off2, Len2), {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, "no!!", Off2, Len2), {error, bad_arg} = ?FLU_C:read_chunk(Host, TcpPort, + ?DUMMY_PV1_EPOCH, BadFile, Off2, Len2), %% We know that File1 still exists. Pretend that we've done a From 6b8a3cf2a4dd3ef5666581c803f3ce87fde78739 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 20:49:45 +0900 Subject: [PATCH 11/17] WIP: epoch ID added to checksum protocol command --- src/machi_admin_util.erl | 59 +++++++++++++++++++++------------- src/machi_flu1.erl | 17 +++++----- src/machi_flu1_client.erl | 22 +++++++------ test/machi_admin_util_test.erl | 6 ++-- test/machi_flu1_test.erl | 7 ++-- 5 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/machi_admin_util.erl b/src/machi_admin_util.erl index ea0f3bf..990d948 100644 --- a/src/machi_admin_util.erl +++ b/src/machi_admin_util.erl @@ -25,9 +25,8 @@ -type inet_port() :: inet:port_number(). -export([ - %% verify_file_checksums_local/2, - verify_file_checksums_local/3, - verify_file_checksums_remote/2, verify_file_checksums_remote/3 + verify_file_checksums_local/3, verify_file_checksums_local/4, + verify_file_checksums_remote/3, verify_file_checksums_remote/4 ]). -compile(export_all). @@ -36,26 +35,41 @@ -define(FLU_C, machi_flu1_client). --spec verify_file_checksums_local(inet_host(), inet_port(), binary()|list()) -> +-spec verify_file_checksums_local(port(), machi_flu1_client:epoch_id(), binary()|list()) -> {ok, [tuple()]} | {error, term()}. -verify_file_checksums_local(Host, TcpPort, Path) -> - Sock1 = machi_util:connect(Host, TcpPort), - verify_file_checksums_local2(Sock1, Path). +verify_file_checksums_local(Sock1, EpochID, Path) when is_port(Sock1) -> + verify_file_checksums_local2(Sock1, EpochID, Path). --spec verify_file_checksums_remote(port(), binary()|list()) -> +-spec verify_file_checksums_local(inet_host(), inet_port(), + machi_flu1_client:epoch_id(), binary()|list()) -> {ok, [tuple()]} | {error, term()}. -verify_file_checksums_remote(Sock1, File) when is_port(Sock1) -> - verify_file_checksums_remote2(Sock1, File). - --spec verify_file_checksums_remote(inet_host(), inet_port(), binary()|list()) -> - {ok, [tuple()]} | {error, term()}. -verify_file_checksums_remote(Host, TcpPort, File) -> +verify_file_checksums_local(Host, TcpPort, EpochID, Path) -> Sock1 = machi_util:connect(Host, TcpPort), - verify_file_checksums_remote2(Sock1, File). + try + verify_file_checksums_local2(Sock1, EpochID, Path) + after + catch gen_tcp:close(Sock1) + end. + +-spec verify_file_checksums_remote(port(), machi_flu1_client:epoch_id(), binary()|list()) -> + {ok, [tuple()]} | {error, term()}. +verify_file_checksums_remote(Sock1, EpochID, File) when is_port(Sock1) -> + verify_file_checksums_remote2(Sock1, EpochID, File). + +-spec verify_file_checksums_remote(inet_host(), inet_port(), + machi_flu1_client:epoch_id(), binary()|list()) -> + {ok, [tuple()]} | {error, term()}. +verify_file_checksums_remote(Host, TcpPort, EpochID, File) -> + Sock1 = machi_util:connect(Host, TcpPort), + try + verify_file_checksums_remote2(Sock1, EpochID, File) + after + catch gen_tcp:close(Sock1) + end. %%%%%%%%%%%%%%%%%%%%%%%%%%% -verify_file_checksums_local2(Sock1, Path0) -> +verify_file_checksums_local2(Sock1, EpochID, Path0) -> Path = machi_util:make_string(Path0), case file:open(Path, [read, binary, raw]) of {ok, FH} -> @@ -64,7 +78,7 @@ verify_file_checksums_local2(Sock1, Path0) -> ReadChunk = fun(_File, Offset, Size) -> file:pread(FH, Offset, Size) end, - verify_file_checksums_common(Sock1, File, ReadChunk) + verify_file_checksums_common(Sock1, EpochID, File, ReadChunk) after file:close(FH) end; @@ -72,18 +86,17 @@ verify_file_checksums_local2(Sock1, Path0) -> Else end. -verify_file_checksums_remote2(Sock1, File) -> +verify_file_checksums_remote2(Sock1, EpochID, File) -> ReadChunk = fun(File_name, Offset, Size) -> - ?FLU_C:read_chunk(Sock1, ?DUMMY_PV1_EPOCH, + ?FLU_C:read_chunk(Sock1, EpochID, File_name, Offset, Size) end, - verify_file_checksums_common(Sock1, File, ReadChunk). + verify_file_checksums_common(Sock1, EpochID, File, ReadChunk). -verify_file_checksums_common(Sock1, File, ReadChunk) -> +verify_file_checksums_common(Sock1, EpochID, File, ReadChunk) -> try - case ?FLU_C:checksum_list(Sock1, File) of + case ?FLU_C:checksum_list(Sock1, EpochID, File) of {ok, Info} -> - ?FLU_C:checksum_list(Sock1, File), Res = lists:foldl(verify_chunk_checksum(File, ReadChunk), [], Info), {ok, Res}; diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 6bea7ec..93bb35e 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -107,10 +107,9 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> case gen_tcp:recv(Sock, 0, 60*1000) of {ok, Line} -> %% machi_util:verb("Got: ~p\n", [Line]), - PrefixLenLF_E = byte_size(Line) - 2 - ?EpochIDSpace - 8 - 1, - FileLenLF_E = byte_size(Line) - 2 - ?EpochIDSpace - 16 - 8 - 1, - CSumFileLenLF = byte_size(Line) - 2 - 1, - CSumFileLenCRLF = byte_size(Line) - 2 - 2, + PrefixLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 8 - 1, + FileLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 16 - 8 - 1, + CSumFileLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 1, WriteFileLenLF = byte_size(Line) - 7 - 16 - 1 - 8 - 1 - 1, DelFileLenLF = byte_size(Line) - 14 - 1, case Line of @@ -118,20 +117,20 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> <<"A ", _EpochIDRaw:(?EpochIDSpace)/binary, LenHex:8/binary, - Prefix:PrefixLenLF_E/binary, "\n">> -> + Prefix:PrefixLenLF/binary, "\n">> -> do_net_server_append(RegName, Sock, LenHex, Prefix); <<"R ", _EpochIDRaw:(?EpochIDSpace)/binary, OffsetHex:16/binary, LenHex:8/binary, - File:FileLenLF_E/binary, "\n">> -> + File:FileLenLF/binary, "\n">> -> do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); <<"L\n">> -> do_net_server_listing(Sock, DataDir); <<"L\r\n">> -> do_net_server_listing(Sock, DataDir); - <<"C ", File:CSumFileLenLF/binary, "\n">> -> - do_net_server_checksum_listing(Sock, File, DataDir); - <<"C ", File:CSumFileLenCRLF/binary, "\n">> -> + <<"C ", + _EpochIDRaw:(?EpochIDSpace)/binary, + File:CSumFileLenLF/binary, "\n">> -> do_net_server_checksum_listing(Sock, File, DataDir); <<"QUIT\n">> -> catch gen_tcp:close(Sock), diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 331b0d1..e0ae3f3 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -25,7 +25,7 @@ -export([ append_chunk/4, append_chunk/5, read_chunk/5, read_chunk/6, - checksum_list/2, checksum_list/3, + checksum_list/3, checksum_list/4, list_files/1, list_files/2, quit/1 ]). @@ -53,6 +53,8 @@ -type file_size() :: non_neg_integer(). -type file_prefix() :: binary() | list(). +-export_type([epoch_id/0]). + %% @doc Append a chunk (binary- or iolist-style) of data to a file %% with `Prefix'. @@ -99,19 +101,19 @@ read_chunk(Host, TcpPort, EpochID, File, Offset, Size) %% @doc Fetch the list of chunk checksums for `File'. --spec checksum_list(port(), file_name()) -> +-spec checksum_list(port(), epoch_id(), file_name()) -> {ok, [chunk_csum()]} | {error, term()}. -checksum_list(Sock, File) when is_port(Sock) -> - checksum_list2(Sock, File). +checksum_list(Sock, EpochID, File) when is_port(Sock) -> + checksum_list2(Sock, EpochID, File). %% @doc Fetch the list of chunk checksums for `File'. --spec checksum_list(inet_host(), inet_port(), file_name()) -> +-spec checksum_list(inet_host(), inet_port(), epoch_id(), file_name()) -> {ok, [chunk_csum()]} | {error, term()}. -checksum_list(Host, TcpPort, File) when is_integer(TcpPort) -> +checksum_list(Host, TcpPort, EpochID, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try - checksum_list2(Sock, File) + checksum_list2(Sock, EpochID, File) after catch gen_tcp:close(Sock) end. @@ -308,9 +310,11 @@ list2({ok, Line}, Sock) -> list2(Else, _Sock) -> throw({server_protocol_error, Else}). -checksum_list2(Sock, File) -> +checksum_list2(Sock, EpochID, File) -> try - ok = gen_tcp:send(Sock, [<<"C ">>, File, <<"\n">>]), + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, + ok = gen_tcp:send(Sock, [<<"C ">>, EpochIDRaw, File, <<"\n">>]), ok = inet:setopts(Sock, [{packet, line}]), case gen_tcp:recv(Sock, 0) of {ok, <<"OK ", Rest/binary>> = Line} -> diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl index dd46af2..de03ccb 100644 --- a/test/machi_admin_util_test.erl +++ b/test/machi_admin_util_test.erl @@ -42,7 +42,7 @@ verify_file_checksums_test() -> X <- lists:seq(1,10)], {ok, [{_FileSize,File}]} = ?FLU_C:list_files(Sock1), {ok, []} = machi_admin_util:verify_file_checksums_remote( - Host, TcpPort, File), + Host, TcpPort, ?DUMMY_PV1_EPOCH, File), Path = DataDir ++ "/" ++ binary_to_list(File), {ok, FH} = file:open(Path, [read,write]), @@ -54,12 +54,12 @@ verify_file_checksums_test() -> %% Check the local flavor of the API {ok, Res1} = machi_admin_util:verify_file_checksums_local( - Host, TcpPort, Path), + Host, TcpPort, ?DUMMY_PV1_EPOCH, Path), 3 = length(Res1), %% Check the remote flavor of the API {ok, Res2} = machi_admin_util:verify_file_checksums_remote( - Host, TcpPort, File), + Host, TcpPort, ?DUMMY_PV1_EPOCH, File), 3 = length(Res2), ok diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index e23f33d..701fe37 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -49,8 +49,10 @@ flu_smoke_test() -> FLU1 = setup_test_flu(smoke_flu, TcpPort, DataDir), try {error, no_such_file} = ?FLU_C:checksum_list(Host, TcpPort, + ?DUMMY_PV1_EPOCH, "does-not-exist"), - {error, bad_arg} = ?FLU_C:checksum_list(Host, TcpPort, BadFile), + {error, bad_arg} = ?FLU_C:checksum_list(Host, TcpPort, + ?DUMMY_PV1_EPOCH, BadFile), {ok, []} = ?FLU_C:list_files(Host, TcpPort), @@ -60,7 +62,8 @@ flu_smoke_test() -> Prefix, Chunk1), {ok, Chunk1} = ?FLU_C:read_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, File1, Off1, Len1), - {ok, [{_,_,_}]} = ?FLU_C:checksum_list(Host, TcpPort, File1), + {ok, [{_,_,_}]} = ?FLU_C:checksum_list(Host, TcpPort, + ?DUMMY_PV1_EPOCH, File1), {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, BadPrefix, Chunk1), From 3aaa2c3a3d0b07f952a176453e35dccad730ab8a Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 21:01:48 +0900 Subject: [PATCH 12/17] WIP: epoch ID added to list protocol command --- src/machi_flu1.erl | 4 +--- src/machi_flu1_client.erl | 30 ++++++++++++++++-------------- test/machi_admin_util_test.erl | 2 +- test/machi_flu1_test.erl | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 93bb35e..5ba5dd9 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -124,9 +124,7 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> OffsetHex:16/binary, LenHex:8/binary, File:FileLenLF/binary, "\n">> -> do_net_server_read(Sock, OffsetHex, LenHex, File, DataDir); - <<"L\n">> -> - do_net_server_listing(Sock, DataDir); - <<"L\r\n">> -> + <<"L ", _EpochIDRaw:(?EpochIDSpace)/binary, "\n">> -> do_net_server_listing(Sock, DataDir); <<"C ", _EpochIDRaw:(?EpochIDSpace)/binary, diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index e0ae3f3..6652ed0 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -26,7 +26,7 @@ append_chunk/4, append_chunk/5, read_chunk/5, read_chunk/6, checksum_list/3, checksum_list/4, - list_files/1, list_files/2, + list_files/2, list_files/3, quit/1 ]). %% For "internal" replication only. @@ -120,19 +120,19 @@ checksum_list(Host, TcpPort, EpochID, File) when is_integer(TcpPort) -> %% @doc Fetch the list of all files on the remote FLU. --spec list_files(port()) -> +-spec list_files(port(), epoch_id()) -> {ok, [file_info()]} | {error, term()}. -list_files(Sock) when is_port(Sock) -> - list2(Sock). +list_files(Sock, EpochID) when is_port(Sock) -> + list2(Sock, EpochID). %% @doc Fetch the list of all files on the remote FLU. --spec list_files(inet_host(), inet_port()) -> +-spec list_files(inet_host(), inet_port(), epoch_id()) -> {ok, [file_info()]} | {error, term()}. -list_files(Host, TcpPort) when is_integer(TcpPort) -> +list_files(Host, TcpPort, EpochID) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try - list2(Sock) + list2(Sock, EpochID) after catch gen_tcp:close(Sock) end. @@ -285,12 +285,14 @@ read_chunk2(Sock, EpochID, File0, Offset, Size) -> end end. -list2(Sock) -> +list2(Sock, EpochID) -> try - ok = gen_tcp:send(Sock, <<"L\n">>), + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, + ok = gen_tcp:send(Sock, [<<"L ">>, EpochIDRaw, <<"\n">>]), ok = inet:setopts(Sock, [{packet, line}]), {ok, <<"OK\n">>} = gen_tcp:recv(Sock, 0), - Res = list2(gen_tcp:recv(Sock, 0), Sock), + Res = list3(gen_tcp:recv(Sock, 0), Sock), ok = inet:setopts(Sock, [{packet, raw}]), {ok, Res} catch @@ -300,14 +302,14 @@ list2(Sock) -> {error, {badmatch, BadMatch}} end. -list2({ok, <<".\n">>}, _Sock) -> +list3({ok, <<".\n">>}, _Sock) -> []; -list2({ok, Line}, Sock) -> +list3({ok, Line}, Sock) -> FileLen = byte_size(Line) - 16 - 1 - 1, <> = Line, Size = machi_util:hexstr_to_int(SizeHex), - [{Size, File}|list2(gen_tcp:recv(Sock, 0), Sock)]; -list2(Else, _Sock) -> + [{Size, File}|list3(gen_tcp:recv(Sock, 0), Sock)]; +list3(Else, _Sock) -> throw({server_protocol_error, Else}). checksum_list2(Sock, EpochID, File) -> diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl index de03ccb..0a44a15 100644 --- a/test/machi_admin_util_test.erl +++ b/test/machi_admin_util_test.erl @@ -40,7 +40,7 @@ verify_file_checksums_test() -> [{ok, _} = ?FLU_C:append_chunk(Sock1, ?DUMMY_PV1_EPOCH, Prefix, <>) || X <- lists:seq(1,10)], - {ok, [{_FileSize,File}]} = ?FLU_C:list_files(Sock1), + {ok, [{_FileSize,File}]} = ?FLU_C:list_files(Sock1, ?DUMMY_PV1_EPOCH), {ok, []} = machi_admin_util:verify_file_checksums_remote( Host, TcpPort, ?DUMMY_PV1_EPOCH, File), diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 701fe37..e98dd6c 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -54,7 +54,7 @@ flu_smoke_test() -> {error, bad_arg} = ?FLU_C:checksum_list(Host, TcpPort, ?DUMMY_PV1_EPOCH, BadFile), - {ok, []} = ?FLU_C:list_files(Host, TcpPort), + {ok, []} = ?FLU_C:list_files(Host, TcpPort, ?DUMMY_PV1_EPOCH), Chunk1 = <<"yo!">>, {ok, {Off1,Len1,File1}} = ?FLU_C:append_chunk(Host, TcpPort, @@ -67,7 +67,7 @@ flu_smoke_test() -> {error, bad_arg} = ?FLU_C:append_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, BadPrefix, Chunk1), - {ok, [{_,File1}]} = ?FLU_C:list_files(Host, TcpPort), + {ok, [{_,File1}]} = ?FLU_C:list_files(Host, TcpPort, ?DUMMY_PV1_EPOCH), Len1 = size(Chunk1), {error, no_such_file} = ?FLU_C:read_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, From 7627ba08a3fea58cb5adc1d4a0a61090300a656e Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Thu, 2 Apr 2015 21:18:41 +0900 Subject: [PATCH 13/17] WIP: epoch ID added to write/delete/trunc protocol commands --- src/machi_flu1.erl | 16 +++++++--- src/machi_flu1_client.erl | 66 ++++++++++++++++++++++----------------- test/machi_flu1_test.erl | 20 +++++++----- 3 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 5ba5dd9..78fc3b9 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -110,8 +110,8 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> PrefixLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 8 - 1, FileLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 16 - 8 - 1, CSumFileLenLF = byte_size(Line) - 2 - ?EpochIDSpace - 1, - WriteFileLenLF = byte_size(Line) - 7 - 16 - 1 - 8 - 1 - 1, - DelFileLenLF = byte_size(Line) - 14 - 1, + WriteFileLenLF = byte_size(Line) - 7 - ?EpochIDSpace - 16 - 8 - 1, + DelFileLenLF = byte_size(Line) - 14 - ?EpochIDSpace - 1, case Line of %% For normal use <<"A ", @@ -137,14 +137,20 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> catch gen_tcp:close(Sock), exit(normal); %% For "internal" replication only. - <<"W-repl ", OffsetHex:16/binary, " ", LenHex:8/binary, " ", + <<"W-repl ", + _EpochIDRaw:(?EpochIDSpace)/binary, + OffsetHex:16/binary, LenHex:8/binary, File:WriteFileLenLF/binary, "\n">> -> do_net_server_write(Sock, OffsetHex, LenHex, File, DataDir); %% For data migration only. - <<"DEL-migration ", File:DelFileLenLF/binary, "\n">> -> + <<"DEL-migration ", + _EpochIDRaw:(?EpochIDSpace)/binary, + File:DelFileLenLF/binary, "\n">> -> do_net_server_delete_migration_only(Sock, File, DataDir); %% For erasure coding hackityhack - <<"TRUNC-hack--- ", File:DelFileLenLF/binary, "\n">> -> + <<"TRUNC-hack--- ", + _EpochIDRaw:(?EpochIDSpace)/binary, + File:DelFileLenLF/binary, "\n">> -> do_net_server_truncate_hackityhack(Sock, File, DataDir); _ -> machi_util:verb("Else Got: ~p\n", [Line]), diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 6652ed0..d999bab 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -31,9 +31,9 @@ ]). %% For "internal" replication only. -export([ - write_chunk/4, write_chunk/5, - delete_migration/2, delete_migration/3, - trunc_hack/2, trunc_hack/3 + write_chunk/5, write_chunk/6, + delete_migration/3, delete_migration/4, + trunc_hack/3, trunc_hack/4 ]). -type chunk() :: binary() | iolist(). % client can use either @@ -151,22 +151,23 @@ quit(Sock) when is_port(Sock) -> %% @doc Restricted API: Write a chunk of already-sequenced data to %% `File' at `Offset'. --spec write_chunk(port(), file_name(), file_offset(), chunk()) -> +-spec write_chunk(port(), epoch_id(), file_name(), file_offset(), chunk()) -> ok | {error, term()}. -write_chunk(Sock, File, Offset, Chunk) +write_chunk(Sock, EpochID, File, Offset, Chunk) when Offset >= ?MINIMUM_OFFSET -> - write_chunk2(Sock, File, Offset, Chunk). + write_chunk2(Sock, EpochID, File, Offset, Chunk). %% @doc Restricted API: Write a chunk of already-sequenced data to %% `File' at `Offset'. --spec write_chunk(inet_host(), inet_port(), file_name(), file_offset(), chunk()) -> +-spec write_chunk(inet_host(), inet_port(), + epoch_id(), file_name(), file_offset(), chunk()) -> ok | {error, term()}. -write_chunk(Host, TcpPort, File, Offset, Chunk) +write_chunk(Host, TcpPort, EpochID, File, Offset, Chunk) when Offset >= ?MINIMUM_OFFSET -> Sock = machi_util:connect(Host, TcpPort), try - write_chunk2(Sock, File, Offset, Chunk) + write_chunk2(Sock, EpochID, File, Offset, Chunk) after catch gen_tcp:close(Sock) end. @@ -174,20 +175,20 @@ write_chunk(Host, TcpPort, File, Offset, Chunk) %% @doc Restricted API: Delete a file after it has been successfully %% migrated. --spec delete_migration(port(), file_name()) -> +-spec delete_migration(port(), epoch_id(), file_name()) -> ok | {error, term()}. -delete_migration(Sock, File) when is_port(Sock) -> - delete_migration2(Sock, File). +delete_migration(Sock, EpochID, File) when is_port(Sock) -> + delete_migration2(Sock, EpochID, File). %% @doc Restricted API: Delete a file after it has been successfully %% migrated. --spec delete_migration(inet_host(), inet_port(), file_name()) -> +-spec delete_migration(inet_host(), inet_port(), epoch_id(), file_name()) -> ok | {error, term()}. -delete_migration(Host, TcpPort, File) when is_integer(TcpPort) -> +delete_migration(Host, TcpPort, EpochID, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try - delete_migration2(Sock, File) + delete_migration2(Sock, EpochID, File) after catch gen_tcp:close(Sock) end. @@ -195,20 +196,20 @@ delete_migration(Host, TcpPort, File) when is_integer(TcpPort) -> %% @doc Restricted API: Truncate a file after it has been successfully %% erasure coded. --spec trunc_hack(port(), file_name()) -> +-spec trunc_hack(port(), epoch_id(), file_name()) -> ok | {error, term()}. -trunc_hack(Sock, File) when is_port(Sock) -> - trunc_hack2(Sock, File). +trunc_hack(Sock, EpochID, File) when is_port(Sock) -> + trunc_hack2(Sock, EpochID, File). %% @doc Restricted API: Truncate a file after it has been successfully %% erasure coded. --spec trunc_hack(inet_host(), inet_port(), file_name()) -> +-spec trunc_hack(inet_host(), inet_port(), epoch_id(), file_name()) -> ok | {error, term()}. -trunc_hack(Host, TcpPort, File) when is_integer(TcpPort) -> +trunc_hack(Host, TcpPort, EpochID, File) when is_integer(TcpPort) -> Sock = machi_util:connect(Host, TcpPort), try - trunc_hack2(Sock, File) + trunc_hack2(Sock, EpochID, File) after catch gen_tcp:close(Sock) end. @@ -365,8 +366,10 @@ checksum_list_finish(Chunks) -> end || Line <- re:split(Bin, "\n", [{return, binary}]), Line /= <<>>]. -write_chunk2(Sock, File0, Offset, Chunk0) -> +write_chunk2(Sock, EpochID, File0, Offset, Chunk0) -> try + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, %% TODO: add client-side checksum to the server's protocol %% _ = crypto:hash(md5, Chunk), File = machi_util:make_binary(File0), @@ -376,9 +379,8 @@ write_chunk2(Sock, File0, Offset, Chunk0) -> Len = iolist_size(Chunk0), true = (Len =< ?MAX_CHUNK_SIZE), LenHex = machi_util:int_to_hexbin(Len, 32), - Cmd = <<"W-repl ", OffsetHex/binary, " ", - LenHex/binary, " ", File/binary, "\n">>, - + Cmd = [<<"W-repl ">>, EpochIDRaw, OffsetHex, + LenHex, File, <<"\n">>], ok = gen_tcp:send(Sock, [Cmd, Chunk]), {ok, Line} = gen_tcp:recv(Sock, 0), PathLen = byte_size(Line) - 3 - 16 - 1 - 1, @@ -397,9 +399,12 @@ write_chunk2(Sock, File0, Offset, Chunk0) -> {error, {badmatch, BadMatch, erlang:get_stacktrace()}} end. -delete_migration2(Sock, File) -> +delete_migration2(Sock, EpochID, File) -> try - ok = gen_tcp:send(Sock, [<<"DEL-migration ">>, File, <<"\n">>]), + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, + Cmd = [<<"DEL-migration ">>, EpochIDRaw, File, <<"\n">>], + ok = gen_tcp:send(Sock, Cmd), ok = inet:setopts(Sock, [{packet, line}]), case gen_tcp:recv(Sock, 0) of {ok, <<"OK\n">>} -> @@ -418,9 +423,12 @@ delete_migration2(Sock, File) -> {error, {badmatch, BadMatch}} end. -trunc_hack2(Sock, File) -> +trunc_hack2(Sock, EpochID, File) -> try - ok = gen_tcp:send(Sock, [<<"TRUNC-hack--- ">>, File, <<"\n">>]), + {EpochNum, EpochCSum} = EpochID, + EpochIDRaw = <>, + Cmd = [<<"TRUNC-hack--- ">>, EpochIDRaw, File, <<"\n">>], + ok = gen_tcp:send(Sock, Cmd), ok = inet:setopts(Sock, [{packet, line}]), case gen_tcp:recv(Sock, 0) of {ok, <<"OK\n">>} -> diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index e98dd6c..0552fad 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -80,8 +80,9 @@ flu_smoke_test() -> Len2 = byte_size(Chunk2), Off2 = ?MINIMUM_OFFSET + 77, File2 = "smoke-prefix", - ok = ?FLU_C:write_chunk(Host, TcpPort, File2, Off2, Chunk2), - {error, bad_arg} = ?FLU_C:write_chunk(Host, TcpPort, + ok = ?FLU_C:write_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, + File2, Off2, Chunk2), + {error, bad_arg} = ?FLU_C:write_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, BadFile, Off2, Chunk2), {ok, Chunk2} = ?FLU_C:read_chunk(Host, TcpPort, ?DUMMY_PV1_EPOCH, File2, Off2, Len2), @@ -94,15 +95,18 @@ flu_smoke_test() -> %% We know that File1 still exists. Pretend that we've done a %% migration and exercise the delete_migration() API. - ok = ?FLU_C:delete_migration(Host, TcpPort, File1), - {error, no_such_file} = ?FLU_C:delete_migration(Host, TcpPort, File1), - {error, bad_arg} = ?FLU_C:delete_migration(Host, TcpPort, BadFile), + ok = ?FLU_C:delete_migration(Host, TcpPort, ?DUMMY_PV1_EPOCH, File1), + {error, no_such_file} = ?FLU_C:delete_migration(Host, TcpPort, + ?DUMMY_PV1_EPOCH, File1), + {error, bad_arg} = ?FLU_C:delete_migration(Host, TcpPort, + ?DUMMY_PV1_EPOCH, BadFile), %% We know that File2 still exists. Pretend that we've done a %% migration and exercise the trunc_hack() API. - ok = ?FLU_C:trunc_hack(Host, TcpPort, File2), - ok = ?FLU_C:trunc_hack(Host, TcpPort, File2), - {error, bad_arg} = ?FLU_C:trunc_hack(Host, TcpPort, BadFile), + ok = ?FLU_C:trunc_hack(Host, TcpPort, ?DUMMY_PV1_EPOCH, File2), + ok = ?FLU_C:trunc_hack(Host, TcpPort, ?DUMMY_PV1_EPOCH, File2), + {error, bad_arg} = ?FLU_C:trunc_hack(Host, TcpPort, + ?DUMMY_PV1_EPOCH, BadFile), ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) after From 7205c5283e0c479933b3191c97a7f8b3b839e386 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Fri, 3 Apr 2015 12:33:47 +0900 Subject: [PATCH 14/17] WIP: client side projection store, 1st API op (write) --- TODO-shortterm.org | 25 +++++++++++++--- src/machi_flu1_client.erl | 62 +++++++++++++++++++++++++++++++++++++-- test/machi_flu1_test.erl | 22 ++++++++++++++ 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/TODO-shortterm.org b/TODO-shortterm.org index d695d93..428bddc 100644 --- a/TODO-shortterm.org +++ b/TODO-shortterm.org @@ -1,14 +1,30 @@ * To Do list -** Done: remove the escript* stuff from machi_util.erl -** Done: Add functions to manipulate 1-chain projections +** DONE remove the escript* stuff from machi_util.erl +** DONE Add functions to manipulate 1-chain projections - Add epoch ID = epoch number + checksum of projection! Done via compare() func. -** TODO Change all protocol ops to add epoch ID -** TODO Move the FLU server to gen_server behavior? +** DONE Change all protocol ops to add epoch ID ** TODO Add projection store to each FLU. + +*** DONE What should the API look like? (borrow from chain mgr PoC?) + +Yeah, I think that's pretty complete. Steal it now, worry later. + +*** DONE Choose protocol & TCP port. Share with get/put? Separate? + +Hrm, I like the idea of having a single TCP port to talk to any single +FLU. + +To make the protocol "easy" to hack, how about using the same basic +method as append/write where there's a variable size blob. But we'll +format that blob as a term_to_binary(). Then dispatch to a single +func, and pattern match Erlang style in that func. + +*** TODO Do it. + ** TODO Change all protocol ops to enforce the epoch ID ** TODO Add projection wedging logic to each FLU. @@ -18,3 +34,4 @@ *** TODO Preserve current test code (leave as-is? tiny changes?) *** TODO Make chain manager code flexible enough to run "real world" or "sim" ** TODO Replace registered name use from FLU write/append dispatcher +** TODO Move the FLU server to gen_server behavior? diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index d999bab..edbf90f 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -21,12 +21,19 @@ -module(machi_flu1_client). -include("machi.hrl"). +-include("machi_projection.hrl"). -export([ + %% File API append_chunk/4, append_chunk/5, read_chunk/5, read_chunk/6, checksum_list/3, checksum_list/4, list_files/2, list_files/3, + + %% Projection API + write_projection/3, write_projection/4, + + %% Common API quit/1 ]). %% For "internal" replication only. @@ -44,14 +51,16 @@ -type epoch_csum() :: binary(). -type epoch_num() :: non_neg_integer(). -type epoch_id() :: {epoch_num(), epoch_csum()}. --type inet_host() :: inet:ip_address() | inet:hostname(). --type inet_port() :: inet:port_number(). -type file_info() :: {file_size(), file_name_s()}. -type file_name() :: binary() | list(). -type file_name_s() :: binary(). % server reply -type file_offset() :: non_neg_integer(). -type file_size() :: non_neg_integer(). -type file_prefix() :: binary() | list(). +-type inet_host() :: inet:ip_address() | inet:hostname(). +-type inet_port() :: inet:port_number(). +-type projection() :: #projection_v1{}. +-type projection_type() :: 'public' | 'private'. -export_type([epoch_id/0]). @@ -137,6 +146,26 @@ list_files(Host, TcpPort, EpochID) when is_integer(TcpPort) -> catch gen_tcp:close(Sock) end. +%% @doc Write a projection `Proj' of type `ProjType'. + +-spec write_projection(port(), projection_type(), projection()) -> + 'ok' | {error, written} | {error, term()}. +write_projection(Sock, ProjType, Proj) -> + write_projection2(Sock, ProjType, Proj). + +%% @doc Write a projection `Proj' of type `ProjType'. + +-spec write_projection(inet_host(), inet_port(), + projection_type(), projection()) -> + 'ok' | {error, written} | {error, term()}. +write_projection(Host, TcpPort, ProjType, Proj) -> + Sock = machi_util:connect(Host, TcpPort), + try + write_projection2(Sock, ProjType, Proj) + after + catch gen_tcp:close(Sock) + end. + %% @doc Quit & close the connection to remote FLU. -spec quit(port()) -> @@ -446,3 +475,32 @@ trunc_hack2(Sock, EpochID, File) -> error:{badmatch,_}=BadMatch -> {error, {badmatch, BadMatch}} end. + +write_projection2(Sock, ProjType, Proj) -> + ProjCmd = {write_projection, ProjType, Proj}, + do_projection_common(Sock, ProjCmd). + +do_projection_common(Sock, ProjCmd) -> + try + ProjCmdBin = term_to_binary(ProjCmd), + Len = iolist_size(ProjCmdBin), + true = (Len =< ?MAX_CHUNK_SIZE), + LenHex = machi_util:int_to_hexbin(Len, 32), + Cmd = [<<"PROJ ">>, LenHex, <<"\n">>], + ok = gen_tcp:send(Sock, [Cmd, ProjCmdBin]), + {ok, Line} = gen_tcp:recv(Sock, 0), + PathLen = byte_size(Line) - 3 - 16 - 1 - 1, + case Line of + <<"OK\n">> -> + ok; + <<"ERROR WRITTEN\n">> -> + {error, written}; + Else -> + {error, Else} + end + catch + throw:Error -> + Error; + error:{badmatch,_}=BadMatch -> + {error, {badmatch, BadMatch, erlang:get_stacktrace()}} + end. diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 0552fad..0334d73 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -113,6 +113,28 @@ flu_smoke_test() -> ok = ?FLU:stop(FLU1) end. +flu_projection_test() -> + Host = "localhost", + TcpPort = 32959, + DataDir = "./data", + Prefix = <<"prefix!">>, + BadPrefix = BadFile = "no/good", + + FLU1 = setup_test_flu(projection_test_flu, TcpPort, DataDir), + try + {error, no_such_file} = ?FLU_C:checksum_list(Host, TcpPort, + ?DUMMY_PV1_EPOCH, + "does-not-exist"), + + P1 = machi_projection:new(1, a, [a], [], [a], [], []), + ok = ?FLU_C:write_projection(Host, TcpPort, public, P1), + {error, written} = ?FLU_C:write_projection(Host, TcpPort, public, P1), + + ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) + after + ok = ?FLU:stop(FLU1) + end. + clean_up_data_dir(DataDir) -> Dir1 = DataDir ++ "/config", Fs1 = filelib:wildcard(Dir1 ++ "/*"), From acf54e3c2171c62738865a42684d2b20247682c7 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Fri, 3 Apr 2015 17:10:52 +0900 Subject: [PATCH 15/17] WIP: client side projection store, 1st API op (write), part II --- src/machi_flu1.erl | 71 ++++++++++-- src/machi_flu1_client.erl | 8 +- src/machi_projection_store.erl | 164 ++++++++++++++++++++++++++++ src/machi_util.erl | 15 ++- test/machi_admin_util_test.erl | 2 +- test/machi_flu1_test.erl | 25 +++-- test/pulse_util/event_logger.erl | 154 ++++++++++++++++++++++++++ test/pulse_util/handle_errors.erl | 174 ++++++++++++++++++++++++++++++ test/pulse_util/lamport_clock.erl | 73 +++++++++++++ 9 files changed, 660 insertions(+), 26 deletions(-) create mode 100644 src/machi_projection_store.erl create mode 100644 test/pulse_util/event_logger.erl create mode 100644 test/pulse_util/handle_errors.erl create mode 100644 test/pulse_util/lamport_clock.erl diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 78fc3b9..8bd710d 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -23,11 +23,14 @@ -include_lib("kernel/include/file.hrl"). -include("machi.hrl"). +-include("machi_projection.hrl"). -export([start_link/1, stop/1]). -record(state, { reg_name :: atom(), + proj_store :: pid(), + append_pid :: pid(), tcp_port :: non_neg_integer(), data_dir :: string(), wedge = true :: 'disabled' | boolean(), @@ -52,10 +55,16 @@ stop(Pid) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%% main2(RegName, TcpPort, DataDir, Rest) -> - S1 = #state{reg_name=RegName, + S0 = #state{reg_name=RegName, tcp_port=TcpPort, data_dir=DataDir, props=Rest}, + AppendPid = start_append_server(S0), + ProjRegName = make_projection_server_regname(RegName), + {ok, ProjectionPid} = + machi_projection_store:start_link(ProjRegName, DataDir, AppendPid), + S1 = S0#state{append_pid=AppendPid, + proj_store=ProjectionPid}, S2 = case proplists:get_value(dbg, Rest) of undefined -> S1; @@ -64,10 +73,18 @@ main2(RegName, TcpPort, DataDir, Rest) -> dbg_props=DbgProps, props=lists:keydelete(dbg, 1, Rest)} end, - AppendPid = start_append_server(S2), ListenPid = start_listen_server(S2), + + Config_e = machi_util:make_config_filename(DataDir, "unused"), + ok = filelib:ensure_dir(Config_e), + {_, Data_e} = machi_util:make_data_filename(DataDir, "unused"), + ok = filelib:ensure_dir(Data_e), + Projection_e = machi_util:make_projection_filename(DataDir, "unused"), + ok = filelib:ensure_dir(Projection_e), + put(flu_reg_name, RegName), put(flu_append_pid, AppendPid), + put(flu_projection_pid, ProjectionPid), put(flu_listen_pid, ListenPid), receive forever -> ok end. @@ -77,6 +94,9 @@ start_listen_server(S) -> start_append_server(S) -> spawn_link(fun() -> run_append_server(S) end). +%% start_projection_server(S) -> +%% spawn_link(fun() -> run_projection_server(S) end). + run_listen_server(#state{tcp_port=TcpPort}=S) -> SockOpts = [{reuseaddr, true}, {mode, binary}, {active, false}, {packet, line}], @@ -97,7 +117,9 @@ append_server_loop(#state{data_dir=DataDir}=S) -> {seq_append, From, Prefix, Chunk, CSum} -> spawn(fun() -> append_server_dispatch(From, Prefix, Chunk, CSum, DataDir) end), - append_server_loop(S) + append_server_loop(S); + {wedge_state_change, Boolean} -> + append_server_loop(S#state{wedge=Boolean}) end. -define(EpochIDSpace, (4+20)). @@ -152,6 +174,8 @@ net_server_loop(Sock, #state{reg_name=RegName, data_dir=DataDir}=S) -> _EpochIDRaw:(?EpochIDSpace)/binary, File:DelFileLenLF/binary, "\n">> -> do_net_server_truncate_hackityhack(Sock, File, DataDir); + <<"PROJ ", LenHex:8/binary, "\n">> -> + do_projection_command(Sock, LenHex, S); _ -> machi_util:verb("Else Got: ~p\n", [Line]), gen_tcp:send(Sock, "ERROR SYNTAX\n"), @@ -249,7 +273,6 @@ do_net_server_readwrite_common2(Sock, OffsetHex, LenHex, FileBin, DataDir, file:close(FH) end; {error, enoent} when OptsHasWrite -> - ok = filelib:ensure_dir(Path), do_net_server_readwrite_common( Sock, OffsetHex, LenHex, FileBin, DataDir, FileOpts, DoItFun); @@ -315,10 +338,11 @@ decode_and_reply_net_server_ec_read_version_a(Sock, Rest) -> ok = gen_tcp:send(Sock, ["ERASURE ", BodyLenHex, " ", Hdr, Body]). do_net_server_listing(Sock, DataDir) -> - Files = filelib:wildcard("*", DataDir) -- ["config"], + {_, WildPath} = machi_util:make_data_filename(DataDir, ""), + Files = filelib:wildcard("*", WildPath), Out = ["OK\n", [begin - {ok, FI} = file:read_file_info(DataDir ++ "/" ++ File), + {ok, FI} = file:read_file_info(WildPath ++ "/" ++ File), Size = FI#file_info.size, SizeBin = <>, [machi_util:bin_to_hexstr(SizeBin), <<" ">>, @@ -453,8 +477,6 @@ start_seq_append_server(Prefix, DataDir) -> run_seq_append_server(Prefix, DataDir) -> true = register(machi_util:make_regname(Prefix), self()), - ok = filelib:ensure_dir(DataDir ++ "/unused"), - ok = filelib:ensure_dir(DataDir ++ "/config/unused"), run_seq_append_server2(Prefix, DataDir). run_seq_append_server2(Prefix, DataDir) -> @@ -521,3 +543,36 @@ seq_append_server_loop(DataDir, Prefix, File, {FHd,FHc}=FH_, FileNum, Offset) -> exit(normal) end. +do_projection_command(Sock, LenHex, S) -> + try + Len = machi_util:hexstr_to_int(LenHex), + ok = inet:setopts(Sock, [{packet, raw}]), + {ok, ProjCmdBin} = gen_tcp:recv(Sock, Len), + ok = inet:setopts(Sock, [{packet, line}]), + ProjCmd = binary_to_term(ProjCmdBin), + case handle_projection_command(ProjCmd, S) of + ok -> + ok = gen_tcp:send(Sock, <<"OK\n">>); + {error, written} -> + ok = gen_tcp:send(Sock, <<"ERROR WRITTEN\n">>); + {error, not_written} -> + ok = gen_tcp:send(Sock, <<"ERROR NOT-WRITTEN\n">>); + Else -> + TODO = list_to_binary(io_lib:format("TODO-YOLO-~w", [Else])), + ok = gen_tcp:send(Sock, [<<"ERROR ">>, TODO, <<"\n">>]) + end + catch + What:Why -> + WHA = list_to_binary(io_lib:format("TODO-YOLO.~w:~w-~w", + [What, Why, erlang:get_stacktrace()])), + _ = (catch gen_tcp:send(Sock, [<<"ERROR ">>, WHA, <<"\n">>])) + end. + +handle_projection_command({write_projection, ProjType, Proj}, + #state{proj_store=ProjStore}) -> + machi_projection_store:write(ProjStore, ProjType, Proj); +handle_projection_command(Else, _S) -> + {error, unknown_cmd, Else}. + +make_projection_server_regname(BaseName) -> + list_to_atom(atom_to_list(BaseName) ++ "_projection"). diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index edbf90f..1965d8a 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -150,7 +150,9 @@ list_files(Host, TcpPort, EpochID) when is_integer(TcpPort) -> -spec write_projection(port(), projection_type(), projection()) -> 'ok' | {error, written} | {error, term()}. -write_projection(Sock, ProjType, Proj) -> +write_projection(Sock, ProjType, Proj) + when ProjType == 'public' orelse ProjType == 'private', + is_record(Proj, projection_v1) -> write_projection2(Sock, ProjType, Proj). %% @doc Write a projection `Proj' of type `ProjType'. @@ -158,7 +160,9 @@ write_projection(Sock, ProjType, Proj) -> -spec write_projection(inet_host(), inet_port(), projection_type(), projection()) -> 'ok' | {error, written} | {error, term()}. -write_projection(Host, TcpPort, ProjType, Proj) -> +write_projection(Host, TcpPort, ProjType, Proj) + when ProjType == 'public' orelse ProjType == 'private', + is_record(Proj, projection_v1) -> Sock = machi_util:connect(Host, TcpPort), try write_projection2(Sock, ProjType, Proj) diff --git a/src/machi_projection_store.erl b/src/machi_projection_store.erl new file mode 100644 index 0000000..526113b --- /dev/null +++ b/src/machi_projection_store.erl @@ -0,0 +1,164 @@ +%% ------------------------------------------------------------------- +%% +%% Copyright (c) 2007-2015 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(machi_projection_store). + +-include("machi_projection.hrl"). + +%% API +-export([ + start_link/3, + write/3, write/4 + ]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, { + public_dir = "" :: string(), + private_dir = "" :: string(), + wedged = true :: boolean(), + wedge_notify_pid :: pid() | atom(), + max_public_epoch = -1 :: non_neg_integer(), + max_private_epoch = -1 :: non_neg_integer() + }). + +start_link(RegName, DataDir, NotifyWedgeStateChanges) -> + gen_server:start_link({local, RegName}, + ?MODULE, [DataDir, NotifyWedgeStateChanges], []). + +write(PidSpec, ProjType, Proj) -> + write(PidSpec, ProjType, Proj, infinity). + +write(PidSpec, ProjType, Proj, Timeout) + when ProjType == 'public' orelse ProjType == 'private', + is_record(Proj, projection_v1) -> + g_call(PidSpec, {write, ProjType, Proj}, Timeout). + +init([DataDir, NotifyWedgeStateChanges]) -> + lclock_init(), + PublicDir = machi_util:make_projection_filename(DataDir, "public"), + PrivateDir = machi_util:make_projection_filename(DataDir, "private"), + ok = filelib:ensure_dir(PublicDir ++ "/ignored"), + ok = filelib:ensure_dir(PrivateDir ++ "/ignored"), + MaxPublicEpoch = find_max_epoch(PublicDir), + MaxPrivateEpoch = find_max_epoch(PrivateDir), + + {ok, #state{public_dir=PublicDir, + private_dir=PrivateDir, + wedged=true, + wedge_notify_pid=NotifyWedgeStateChanges, + max_public_epoch=MaxPublicEpoch, + max_private_epoch=MaxPrivateEpoch}}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +g_call(PidSpec, Arg, Timeout) -> + LC1 = lclock_get(), + {Res, LC2} = gen_server:call(PidSpec, {Arg, LC1}, Timeout), + lclock_update(LC2), + Res. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +handle_call({{write, ProjType, Proj}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + {Reply, NewS} = do_proj_write(ProjType, Proj, S), + {reply, {Reply, LC2}, NewS}; +handle_call(_Request, _From, S) -> + Reply = whaaaaaaaaaaaaa, + {reply, Reply, S}. + +handle_cast(_Msg, S) -> + {noreply, S}. + +handle_info(_Info, S) -> + {noreply, S}. + +terminate(_Reason, _S) -> + ok. + +code_change(_OldVsn, S, _Extra) -> + {ok, S}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +do_proj_write(ProjType, #projection_v1{epoch_number=Epoch}=Proj, S) -> + %% TODO: We probably ought to check the projection checksum for sanity, eh? + Dir = pick_path(ProjType, S), + Path = filename:join(Dir, epoch2name(Epoch)), + case file:read_file_info(Path) of + {ok, _FI} -> + {{error, written}, S}; + {error, enoent} -> + {ok, FH} = file:open(Path, [write, raw, binary]), + ok = file:write(FH, term_to_binary(Proj)), + ok = file:sync(FH), + ok = file:close(FH), + {ok, S}; + {error, Else} -> + {{error, Else}, S} + end. + +pick_path(public, S) -> + S#state.public_dir; +pick_path(private, S) -> + S#state.private_dir. + +epoch2name(Epoch) -> + machi_util:int_to_hexstr(Epoch, 32). + +name2epoch(Name) -> + machi_util:hexstr_to_int(Name). + +find_max_epoch(Dir) -> + Fs = lists:sort(filelib:wildcard("*", Dir)), + if Fs == [] -> + -1; + true -> + name2epoch(lists:last(Fs)) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-ifdef(TEST). + +lclock_init() -> + lamport_clock:init(). + +lclock_get() -> + lamport_clock:get(). + +lclock_update(LC) -> + lamport_clock:update(LC). + +-else. % TEST + +lclock_init() -> + ok. + +lclock_get() -> + ok. + +lclock_update(_LC) -> + ok. + +-endif. % TEST diff --git a/src/machi_util.erl b/src/machi_util.erl index f526803..1331d11 100644 --- a/src/machi_util.erl +++ b/src/machi_util.erl @@ -27,6 +27,7 @@ make_binary/1, make_string/1, make_regname/1, make_checksum_filename/2, make_data_filename/2, + make_projection_filename/2, read_max_filenum/2, increment_max_filenum/2, info_msg/2, verb/1, verb/2, %% TCP protocol helpers @@ -60,19 +61,29 @@ make_checksum_filename(DataDir, Prefix, SequencerName, FileNum) -> lists:flatten(io_lib:format("~s/config/~s.~s.~w.csum", [DataDir, Prefix, SequencerName, FileNum])). +make_checksum_filename(DataDir, "") -> + lists:flatten(io_lib:format("~s/config", [DataDir])); make_checksum_filename(DataDir, FileName) -> lists:flatten(io_lib:format("~s/config/~s.csum", [DataDir, FileName])). +make_data_filename(DataDir, "") -> + FullPath = lists:flatten(io_lib:format("~s/data", [DataDir])), + {"", FullPath}; make_data_filename(DataDir, File) -> - FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])), + FullPath = lists:flatten(io_lib:format("~s/data/~s", [DataDir, File])), {File, FullPath}. make_data_filename(DataDir, Prefix, SequencerName, FileNum) -> File = erlang:iolist_to_binary(io_lib:format("~s.~s.~w", [Prefix, SequencerName, FileNum])), - FullPath = lists:flatten(io_lib:format("~s/~s", [DataDir, File])), + FullPath = lists:flatten(io_lib:format("~s/data/~s", [DataDir, File])), {File, FullPath}. +make_projection_filename(DataDir, "") -> + lists:flatten(io_lib:format("~s/projection", [DataDir])); +make_projection_filename(DataDir, File) -> + lists:flatten(io_lib:format("~s/projection/~s", [DataDir, File])). + read_max_filenum(DataDir, Prefix) -> case file:read_file_info(make_config_filename(DataDir, Prefix)) of {error, enoent} -> diff --git a/test/machi_admin_util_test.erl b/test/machi_admin_util_test.erl index 0a44a15..8555959 100644 --- a/test/machi_admin_util_test.erl +++ b/test/machi_admin_util_test.erl @@ -44,7 +44,7 @@ verify_file_checksums_test() -> {ok, []} = machi_admin_util:verify_file_checksums_remote( Host, TcpPort, ?DUMMY_PV1_EPOCH, File), - Path = DataDir ++ "/" ++ binary_to_list(File), + {_, Path} = machi_util:make_data_filename(DataDir,binary_to_list(File)), {ok, FH} = file:open(Path, [read,write]), {ok, _} = file:position(FH, ?MINIMUM_OFFSET), ok = file:write(FH, "y"), diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 0334d73..3a36800 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -37,6 +37,10 @@ setup_test_flu(RegName, TcpPort, DataDir, DbgProps) -> {ok, FLU1} = ?FLU:start_link([{RegName, TcpPort, DataDir}, {dbg, DbgProps}]), + %% TODO the process structuring/racy-ness of the various processes + %% of the FLU needs to be deterministic to remove this sleep race + %% "prevention". + timer:sleep(10), FLU1. flu_smoke_test() -> @@ -113,22 +117,18 @@ flu_smoke_test() -> ok = ?FLU:stop(FLU1) end. -flu_projection_test() -> +flu_projection_smoke_test() -> Host = "localhost", TcpPort = 32959, DataDir = "./data", - Prefix = <<"prefix!">>, - BadPrefix = BadFile = "no/good", FLU1 = setup_test_flu(projection_test_flu, TcpPort, DataDir), try - {error, no_such_file} = ?FLU_C:checksum_list(Host, TcpPort, - ?DUMMY_PV1_EPOCH, - "does-not-exist"), - P1 = machi_projection:new(1, a, [a], [], [a], [], []), ok = ?FLU_C:write_projection(Host, TcpPort, public, P1), {error, written} = ?FLU_C:write_projection(Host, TcpPort, public, P1), + ok = ?FLU_C:write_projection(Host, TcpPort, private, P1), + {error, written} = ?FLU_C:write_projection(Host, TcpPort, private, P1), ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) after @@ -136,12 +136,11 @@ flu_projection_test() -> end. clean_up_data_dir(DataDir) -> - Dir1 = DataDir ++ "/config", - Fs1 = filelib:wildcard(Dir1 ++ "/*"), - [file:delete(F) || F <- Fs1], - _ = file:del_dir(Dir1), - Fs2 = filelib:wildcard(DataDir ++ "/*"), - [file:delete(F) || F <- Fs2], + [begin + Fs = filelib:wildcard(DataDir ++ Glob), + [file:delete(F) || F <- Fs], + [file:del_dir(F) || F <- Fs] + end || Glob <- ["*/*/*/*", "*/*/*", "*/*", "*"] ], _ = file:del_dir(DataDir), ok. diff --git a/test/pulse_util/event_logger.erl b/test/pulse_util/event_logger.erl new file mode 100644 index 0000000..f6a39d0 --- /dev/null +++ b/test/pulse_util/event_logger.erl @@ -0,0 +1,154 @@ +%% ------------------------------------------------------------------- +%% +%% Machi: a small village of replicated files +%% +%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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. +%% +%% ------------------------------------------------------------------- +%%% File : handle_errors.erl +%%% Author : Ulf Norell +%%% Description : +%%% Created : 26 Mar 2012 by Ulf Norell +-module(event_logger). + +-compile(export_all). + +-behaviour(gen_server). + +%% API +-export([start_link/0, event/1, event/2, get_events/0, start_logging/0]). +-export([timestamp/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, { start_time, events = [] }). + +-record(event, { timestamp, data }). + + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +start_logging() -> + gen_server:call(?MODULE, {start, timestamp()}). + +event(EventData) -> + event(EventData, timestamp()). + +event(EventData, Timestamp) -> + gen_server:call(?MODULE, + #event{ timestamp = Timestamp, data = EventData }). + +async_event(EventData) -> + gen_server:cast(?MODULE, + #event{ timestamp = timestamp(), data = EventData }). + +get_events() -> + gen_server:call(?MODULE, get_events). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([]) -> + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> +%% {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(Event = #event{}, _From, State) -> + {reply, ok, add_event(Event, State)}; +handle_call({start, Now}, _From, S) -> + {reply, ok, S#state{ events = [], start_time = Now }}; +handle_call(get_events, _From, S) -> + {reply, lists:reverse([ {E#event.timestamp, E#event.data} || E <- S#state.events]), + S#state{ events = [] }}; +handle_call(Request, _From, State) -> + {reply, {error, {bad_call, Request}}, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(Event = #event{}, State) -> + {noreply, add_event(Event, State)}; +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +add_event(#event{timestamp = Now, data = Data}, State) -> + Event = #event{ timestamp = Now, data = Data }, + State#state{ events = [Event|State#state.events] }. + +timestamp() -> + lamport_clock:get(). diff --git a/test/pulse_util/handle_errors.erl b/test/pulse_util/handle_errors.erl new file mode 100644 index 0000000..97965b8 --- /dev/null +++ b/test/pulse_util/handle_errors.erl @@ -0,0 +1,174 @@ +%% ------------------------------------------------------------------- +%% +%% Machi: a small village of replicated files +%% +%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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. +%% +%% ------------------------------------------------------------------- +%%%------------------------------------------------------------------- +%%% @author Hans Svensson <> +%%% @copyright (C) 2012, Hans Svensson +%%% @doc +%%% +%%% @end +%%% Created : 19 Mar 2012 by Hans Svensson <> +%%%------------------------------------------------------------------- +-module(handle_errors). + +-behaviour(gen_event). + +%% API +-export([start_link/0, add_handler/0]). + +%% gen_event callbacks +-export([init/1, handle_event/2, handle_call/2, + handle_info/2, terminate/2, code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, { errors = [] }). + +%%%=================================================================== +%%% gen_event callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @doc +%% Creates an event manager +%% +%% @spec start_link() -> {ok, Pid} | {error, Error} +%% @end +%%-------------------------------------------------------------------- +start_link() -> + gen_event:start_link({local, ?SERVER}). + +%%-------------------------------------------------------------------- +%% @doc +%% Adds an event handler +%% +%% @spec add_handler() -> ok | {'EXIT', Reason} | term() +%% @end +%%-------------------------------------------------------------------- +add_handler() -> + gen_event:add_handler(?SERVER, ?MODULE, []). + +%%%=================================================================== +%%% gen_event callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever a new event handler is added to an event manager, +%% this function is called to initialize the event handler. +%% +%% @spec init(Args) -> {ok, State} +%% @end +%%-------------------------------------------------------------------- +init([]) -> + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever an event manager receives an event sent using +%% gen_event:notify/2 or gen_event:sync_notify/2, this function is +%% called for each installed event handler to handle the event. +%% +%% @spec handle_event(Event, State) -> +%% {ok, State} | +%% {swap_handler, Args1, State1, Mod2, Args2} | +%% remove_handler +%% @end +%%-------------------------------------------------------------------- +handle_event({error, _, {_, "Hintfile '~s' has bad CRC" ++ _, _}}, State) -> + {ok, State}; +handle_event({error, _, {_, "** Generic server" ++ _, _}}, State) -> + {ok, State}; +handle_event({error, _, {_, "Failed to merge ~p: ~p\n", [_, not_ready]}}, State) -> + {ok, State}; +handle_event({error, _, {_, "Failed to merge ~p: ~p\n", [_, {merge_locked, _, _}]}}, State) -> + {ok, State}; +handle_event({error, _, {_, "Failed to read lock data from ~s: ~p\n", [_, {invalid_data, <<>>}]}}, State) -> + {ok, State}; +handle_event({error, _, Event}, State) -> + {ok, State#state{ errors = [Event|State#state.errors] }}; +handle_event(_Event, State) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever an event manager receives a request sent using +%% gen_event:call/3,4, this function is called for the specified +%% event handler to handle the request. +%% +%% @spec handle_call(Request, State) -> +%% {ok, Reply, State} | +%% {swap_handler, Reply, Args1, State1, Mod2, Args2} | +%% {remove_handler, Reply} +%% @end +%%-------------------------------------------------------------------- +handle_call(get_errors, S) -> + {ok, S#state.errors, S#state{ errors = [] }}; +handle_call(_Request, State) -> + Reply = ok, + {ok, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called for each installed event handler when +%% an event manager receives any other message than an event or a +%% synchronous request (or a system message). +%% +%% @spec handle_info(Info, State) -> +%% {ok, State} | +%% {swap_handler, Args1, State1, Mod2, Args2} | +%% remove_handler +%% @end +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Whenever an event handler is deleted from an event manager, this +%% function is called. It should be the opposite of Module:init/1 and +%% do any necessary cleaning up. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/test/pulse_util/lamport_clock.erl b/test/pulse_util/lamport_clock.erl new file mode 100644 index 0000000..0bb8e3d --- /dev/null +++ b/test/pulse_util/lamport_clock.erl @@ -0,0 +1,73 @@ +%% ------------------------------------------------------------------- +%% +%% Machi: a small village of replicated files +%% +%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(lamport_clock). + +-export([init/0, reset/0, get/0, update/1, incr/0]). + +-define(KEY, ?MODULE). + +-ifdef(TEST). + +init() -> + case get(?KEY) of + undefined -> + reset(); + N when is_integer(N) -> + ok + end. + +reset() -> + FakeTOD = 0, + put(?KEY, FakeTOD + 1). + +get() -> + init(), + get(?KEY). + +update(Remote) -> + New = erlang:max(get(?KEY), Remote) + 1, + put(?KEY, New), + New. + +incr() -> + New = get(?KEY) + 1, + put(?KEY, New), + New. + +-else. % TEST + +init() -> + ok. + +reset() -> + ok. + +get() -> + ok. + +update(_) -> + ok. + +incr() -> + ok. + +-endif. % TEST From 022b9c4d1fa546c3999f453e326c49809ab2f6fa Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Fri, 3 Apr 2015 17:55:35 +0900 Subject: [PATCH 16/17] WIP: projection store: read, get latest epoch --- src/machi_flu1.erl | 24 ++++++------ src/machi_flu1_client.erl | 70 ++++++++++++++++++++++++++++++---- src/machi_projection_store.erl | 43 ++++++++++++++++++++- test/machi_flu1_test.erl | 8 ++++ 4 files changed, 125 insertions(+), 20 deletions(-) diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 8bd710d..2dfbce4 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -550,24 +550,26 @@ do_projection_command(Sock, LenHex, S) -> {ok, ProjCmdBin} = gen_tcp:recv(Sock, Len), ok = inet:setopts(Sock, [{packet, line}]), ProjCmd = binary_to_term(ProjCmdBin), - case handle_projection_command(ProjCmd, S) of - ok -> - ok = gen_tcp:send(Sock, <<"OK\n">>); - {error, written} -> - ok = gen_tcp:send(Sock, <<"ERROR WRITTEN\n">>); - {error, not_written} -> - ok = gen_tcp:send(Sock, <<"ERROR NOT-WRITTEN\n">>); - Else -> - TODO = list_to_binary(io_lib:format("TODO-YOLO-~w", [Else])), - ok = gen_tcp:send(Sock, [<<"ERROR ">>, TODO, <<"\n">>]) - end + put(hack, ProjCmd), + Res = handle_projection_command(ProjCmd, S), + ResBin = term_to_binary(Res), + ResLenHex = machi_util:int_to_hexbin(byte_size(ResBin), 32), + ok = gen_tcp:send(Sock, [<<"OK ">>, ResLenHex, <<"\n">>, ResBin]) catch What:Why -> + io:format(user, "OOPS ~p\n", [get(hack)]), + io:format(user, "OOPS ~p ~p ~p\n", [What, Why, erlang:get_stacktrace()]), WHA = list_to_binary(io_lib:format("TODO-YOLO.~w:~w-~w", [What, Why, erlang:get_stacktrace()])), _ = (catch gen_tcp:send(Sock, [<<"ERROR ">>, WHA, <<"\n">>])) end. +handle_projection_command({get_latest_epoch, ProjType}, + #state{proj_store=ProjStore}) -> + machi_projection_store:get_latest_epoch(ProjStore, ProjType); +handle_projection_command({read_projection, ProjType, Epoch}, + #state{proj_store=ProjStore}) -> + machi_projection_store:read(ProjStore, ProjType, Epoch); handle_projection_command({write_projection, ProjType, Proj}, #state{proj_store=ProjStore}) -> machi_projection_store:write(ProjStore, ProjType, Proj); diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 1965d8a..93124dd 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -31,6 +31,8 @@ list_files/2, list_files/3, %% Projection API + get_latest_epoch/2, get_latest_epoch/3, + read_projection/3, read_projection/4, write_projection/3, write_projection/4, %% Common API @@ -146,6 +148,50 @@ list_files(Host, TcpPort, EpochID) when is_integer(TcpPort) -> catch gen_tcp:close(Sock) end. +%% @doc Get the latest epoch number from the FLU's projection store. + +-spec get_latest_epoch(port(), projection_type()) -> + {ok, -1|non_neg_integer()} | {error, term()}. +get_latest_epoch(Sock, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + get_latest_epoch2(Sock, ProjType). + +%% @doc Get the latest epoch number from the FLU's projection store. + +-spec get_latest_epoch(inet_host(), inet_port(), + projection_type()) -> + {ok, -1|non_neg_integer()} | {error, term()}. +get_latest_epoch(Host, TcpPort, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + Sock = machi_util:connect(Host, TcpPort), + try + get_latest_epoch2(Sock, ProjType) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Read a projection `Proj' of type `ProjType'. + +-spec read_projection(port(), projection_type(), epoch_num()) -> + 'ok' | {error, written} | {error, term()}. +read_projection(Sock, ProjType, Epoch) + when ProjType == 'public' orelse ProjType == 'private' -> + read_projection2(Sock, ProjType, Epoch). + +%% @doc Read a projection `Proj' of type `ProjType'. + +-spec read_projection(inet_host(), inet_port(), + projection_type(), epoch_num()) -> + 'ok' | {error, written} | {error, term()}. +read_projection(Host, TcpPort, ProjType, Epoch) + when ProjType == 'public' orelse ProjType == 'private' -> + Sock = machi_util:connect(Host, TcpPort), + try + read_projection2(Sock, ProjType, Epoch) + after + catch gen_tcp:close(Sock) + end. + %% @doc Write a projection `Proj' of type `ProjType'. -spec write_projection(port(), projection_type(), projection()) -> @@ -480,9 +526,17 @@ trunc_hack2(Sock, EpochID, File) -> {error, {badmatch, BadMatch}} end. +get_latest_epoch2(Sock, ProjType) -> + ProjCmd = {get_latest_epoch, ProjType}, + do_projection_common(Sock, ProjCmd). + +read_projection2(Sock, ProjType, Epoch) -> + ProjCmd = {read_projection, ProjType, Epoch}, + do_projection_common(Sock, ProjCmd). + write_projection2(Sock, ProjType, Proj) -> - ProjCmd = {write_projection, ProjType, Proj}, - do_projection_common(Sock, ProjCmd). + ProjCmd = {write_projection, ProjType, Proj}, + do_projection_common(Sock, ProjCmd). do_projection_common(Sock, ProjCmd) -> try @@ -492,13 +546,15 @@ do_projection_common(Sock, ProjCmd) -> LenHex = machi_util:int_to_hexbin(Len, 32), Cmd = [<<"PROJ ">>, LenHex, <<"\n">>], ok = gen_tcp:send(Sock, [Cmd, ProjCmdBin]), + ok = inet:setopts(Sock, [{packet, line}]), {ok, Line} = gen_tcp:recv(Sock, 0), - PathLen = byte_size(Line) - 3 - 16 - 1 - 1, case Line of - <<"OK\n">> -> - ok; - <<"ERROR WRITTEN\n">> -> - {error, written}; + <<"OK ", ResLenHex:8/binary, "\n">> -> + ResLen = machi_util:hexstr_to_int(ResLenHex), + ok = inet:setopts(Sock, [{packet, raw}]), + {ok, ResBin} = gen_tcp:recv(Sock, ResLen), + ok = inet:setopts(Sock, [{packet, line}]), + binary_to_term(ResBin); Else -> {error, Else} end diff --git a/src/machi_projection_store.erl b/src/machi_projection_store.erl index 526113b..a2bf4d7 100644 --- a/src/machi_projection_store.erl +++ b/src/machi_projection_store.erl @@ -25,6 +25,8 @@ %% API -export([ start_link/3, + get_latest_epoch/2, get_latest_epoch/3, + read/3, read/4, write/3, write/4 ]). @@ -37,14 +39,28 @@ private_dir = "" :: string(), wedged = true :: boolean(), wedge_notify_pid :: pid() | atom(), - max_public_epoch = -1 :: non_neg_integer(), - max_private_epoch = -1 :: non_neg_integer() + max_public_epoch = -1 :: -1 | non_neg_integer(), + max_private_epoch = -1 :: -1 | non_neg_integer() }). start_link(RegName, DataDir, NotifyWedgeStateChanges) -> gen_server:start_link({local, RegName}, ?MODULE, [DataDir, NotifyWedgeStateChanges], []). +get_latest_epoch(PidSpec, ProjType) -> + get_latest_epoch(PidSpec, ProjType, infinity). + +get_latest_epoch(PidSpec, ProjType, Timeout) + when ProjType == 'public' orelse ProjType == 'private' -> + g_call(PidSpec, {get_latest_epoch, ProjType}, Timeout). + +read(PidSpec, ProjType, Epoch) -> + read(PidSpec, ProjType, Epoch, infinity). + +read(PidSpec, ProjType, Epoch, Timeout) + when ProjType == 'public' orelse ProjType == 'private' -> + g_call(PidSpec, {read, ProjType, Epoch}, Timeout). + write(PidSpec, ProjType, Proj) -> write(PidSpec, ProjType, Proj, infinity). @@ -79,6 +95,16 @@ g_call(PidSpec, Arg, Timeout) -> %%%%%%%%%%%%%%%%%%%%%%%%%%% +handle_call({{get_latest_epoch, ProjType}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + Epoch = if ProjType == public -> S#state.max_public_epoch; + ProjType == private -> S#state.max_private_epoch + end, + {reply, {{ok, Epoch}, LC2}, S}; +handle_call({{read, ProjType, Epoch}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + {Reply, NewS} = do_proj_read(ProjType, Epoch, S), + {reply, {Reply, LC2}, NewS}; handle_call({{write, ProjType, Proj}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {Reply, NewS} = do_proj_write(ProjType, Proj, S), @@ -101,6 +127,19 @@ code_change(_OldVsn, S, _Extra) -> %%%%%%%%%%%%%%%%%%%%%%%%%%% +do_proj_read(ProjType, Epoch, S) -> + Dir = pick_path(ProjType, S), + Path = filename:join(Dir, epoch2name(Epoch)), + case file:read_file(Path) of + {ok, Bin} -> + %% TODO and if Bin is corrupt? (even if binary_to_term() succeeds) + {{ok, binary_to_term(Bin)}, S}; + {error, enoent} -> + {{error, not_written}, S}; + {error, Else} -> + {{error, Else}, S} + end. + do_proj_write(ProjType, #projection_v1{epoch_number=Epoch}=Proj, S) -> %% TODO: We probably ought to check the projection checksum for sanity, eh? Dir = pick_path(ProjType, S), diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 3a36800..3cd1daf 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -124,11 +124,19 @@ flu_projection_smoke_test() -> FLU1 = setup_test_flu(projection_test_flu, TcpPort, DataDir), try + {ok, -1} = ?FLU_C:get_latest_epoch(Host, TcpPort, public), + {ok, -1} = ?FLU_C:get_latest_epoch(Host, TcpPort, private), + P1 = machi_projection:new(1, a, [a], [], [a], [], []), ok = ?FLU_C:write_projection(Host, TcpPort, public, P1), {error, written} = ?FLU_C:write_projection(Host, TcpPort, public, P1), + {ok, P1} = ?FLU_C:read_projection(Host, TcpPort, public, 1), + {error, not_written} = ?FLU_C:read_projection(Host, TcpPort, public, 2), + ok = ?FLU_C:write_projection(Host, TcpPort, private, P1), {error, written} = ?FLU_C:write_projection(Host, TcpPort, private, P1), + {ok, P1} = ?FLU_C:read_projection(Host, TcpPort, private, 1), + {error, not_written} = ?FLU_C:read_projection(Host, TcpPort, private, 2), ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) after From c27aa1f579fc0383bae19d892c7dafb6aea2d096 Mon Sep 17 00:00:00 2001 From: Scott Lystig Fritchie Date: Fri, 3 Apr 2015 18:37:09 +0900 Subject: [PATCH 17/17] Projection store API complete, I think --- src/machi_flu1.erl | 9 ++++ src/machi_flu1_client.erl | 85 +++++++++++++++++++++++++++++++- src/machi_projection_store.erl | 90 ++++++++++++++++++++++++++++------ test/machi_flu1_test.erl | 29 +++++------ 4 files changed, 183 insertions(+), 30 deletions(-) diff --git a/src/machi_flu1.erl b/src/machi_flu1.erl index 2dfbce4..02f7925 100644 --- a/src/machi_flu1.erl +++ b/src/machi_flu1.erl @@ -567,12 +567,21 @@ do_projection_command(Sock, LenHex, S) -> handle_projection_command({get_latest_epoch, ProjType}, #state{proj_store=ProjStore}) -> machi_projection_store:get_latest_epoch(ProjStore, ProjType); +handle_projection_command({read_latest_projection, ProjType}, + #state{proj_store=ProjStore}) -> + machi_projection_store:read_latest_projection(ProjStore, ProjType); handle_projection_command({read_projection, ProjType, Epoch}, #state{proj_store=ProjStore}) -> machi_projection_store:read(ProjStore, ProjType, Epoch); handle_projection_command({write_projection, ProjType, Proj}, #state{proj_store=ProjStore}) -> machi_projection_store:write(ProjStore, ProjType, Proj); +handle_projection_command({get_all, ProjType}, + #state{proj_store=ProjStore}) -> + machi_projection_store:get_all(ProjStore, ProjType); +handle_projection_command({list_all, ProjType}, + #state{proj_store=ProjStore}) -> + machi_projection_store:list_all(ProjStore, ProjType); handle_projection_command(Else, _S) -> {error, unknown_cmd, Else}. diff --git a/src/machi_flu1_client.erl b/src/machi_flu1_client.erl index 93124dd..6dd6c65 100644 --- a/src/machi_flu1_client.erl +++ b/src/machi_flu1_client.erl @@ -32,8 +32,11 @@ %% Projection API get_latest_epoch/2, get_latest_epoch/3, + read_latest_projection/2, read_latest_projection/3, read_projection/3, read_projection/4, write_projection/3, write_projection/4, + get_all/2, get_all/3, + list_all/2, list_all/3, %% Common API quit/1 @@ -170,10 +173,32 @@ get_latest_epoch(Host, TcpPort, ProjType) catch gen_tcp:close(Sock) end. +%% @doc Get the latest epoch number from the FLU's projection store. + +-spec read_latest_projection(port(), projection_type()) -> + {ok, projection()} | {error, not_written} | {error, term()}. +read_latest_projection(Sock, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + read_latest_projection2(Sock, ProjType). + +%% @doc Get the latest epoch number from the FLU's projection store. + +-spec read_latest_projection(inet_host(), inet_port(), + projection_type()) -> + {ok, projection()} | {error, not_written} | {error, term()}. +read_latest_projection(Host, TcpPort, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + Sock = machi_util:connect(Host, TcpPort), + try + read_latest_projection2(Sock, ProjType) + after + catch gen_tcp:close(Sock) + end. + %% @doc Read a projection `Proj' of type `ProjType'. -spec read_projection(port(), projection_type(), epoch_num()) -> - 'ok' | {error, written} | {error, term()}. + {ok, projection()} | {error, written} | {error, term()}. read_projection(Sock, ProjType, Epoch) when ProjType == 'public' orelse ProjType == 'private' -> read_projection2(Sock, ProjType, Epoch). @@ -182,7 +207,7 @@ read_projection(Sock, ProjType, Epoch) -spec read_projection(inet_host(), inet_port(), projection_type(), epoch_num()) -> - 'ok' | {error, written} | {error, term()}. + {ok, projection()} | {error, written} | {error, term()}. read_projection(Host, TcpPort, ProjType, Epoch) when ProjType == 'public' orelse ProjType == 'private' -> Sock = machi_util:connect(Host, TcpPort), @@ -216,6 +241,50 @@ write_projection(Host, TcpPort, ProjType, Proj) catch gen_tcp:close(Sock) end. +%% @doc Get all projections from the FLU's projection store. + +-spec get_all(port(), projection_type()) -> + {ok, [projection()]} | {error, term()}. +get_all(Sock, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + get_all2(Sock, ProjType). + +%% @doc Get all projections from the FLU's projection store. + +-spec get_all(inet_host(), inet_port(), + projection_type()) -> + {ok, [projection()]} | {error, term()}. +get_all(Host, TcpPort, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + Sock = machi_util:connect(Host, TcpPort), + try + get_all2(Sock, ProjType) + after + catch gen_tcp:close(Sock) + end. + +%% @doc Get all epoch numbers from the FLU's projection store. + +-spec list_all(port(), projection_type()) -> + {ok, [non_neg_integer()]} | {error, term()}. +list_all(Sock, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + list_all2(Sock, ProjType). + +%% @doc Get all epoch numbers from the FLU's projection store. + +-spec list_all(inet_host(), inet_port(), + projection_type()) -> + {ok, [non_neg_integer()]} | {error, term()}. +list_all(Host, TcpPort, ProjType) + when ProjType == 'public' orelse ProjType == 'private' -> + Sock = machi_util:connect(Host, TcpPort), + try + list_all2(Sock, ProjType) + after + catch gen_tcp:close(Sock) + end. + %% @doc Quit & close the connection to remote FLU. -spec quit(port()) -> @@ -530,6 +599,10 @@ get_latest_epoch2(Sock, ProjType) -> ProjCmd = {get_latest_epoch, ProjType}, do_projection_common(Sock, ProjCmd). +read_latest_projection2(Sock, ProjType) -> + ProjCmd = {read_latest_projection, ProjType}, + do_projection_common(Sock, ProjCmd). + read_projection2(Sock, ProjType, Epoch) -> ProjCmd = {read_projection, ProjType, Epoch}, do_projection_common(Sock, ProjCmd). @@ -538,6 +611,14 @@ write_projection2(Sock, ProjType, Proj) -> ProjCmd = {write_projection, ProjType, Proj}, do_projection_common(Sock, ProjCmd). +get_all2(Sock, ProjType) -> + ProjCmd = {get_all, ProjType}, + do_projection_common(Sock, ProjCmd). + +list_all2(Sock, ProjType) -> + ProjCmd = {list_all, ProjType}, + do_projection_common(Sock, ProjCmd). + do_projection_common(Sock, ProjCmd) -> try ProjCmdBin = term_to_binary(ProjCmd), diff --git a/src/machi_projection_store.erl b/src/machi_projection_store.erl index a2bf4d7..c88a21b 100644 --- a/src/machi_projection_store.erl +++ b/src/machi_projection_store.erl @@ -26,8 +26,11 @@ -export([ start_link/3, get_latest_epoch/2, get_latest_epoch/3, + read_latest_projection/2, read_latest_projection/3, read/3, read/4, - write/3, write/4 + write/3, write/4, + get_all/2, get_all/3, + list_all/2, list_all/3 ]). %% gen_server callbacks @@ -54,11 +57,19 @@ get_latest_epoch(PidSpec, ProjType, Timeout) when ProjType == 'public' orelse ProjType == 'private' -> g_call(PidSpec, {get_latest_epoch, ProjType}, Timeout). +read_latest_projection(PidSpec, ProjType) -> + read_latest_projection(PidSpec, ProjType, infinity). + +read_latest_projection(PidSpec, ProjType, Timeout) + when ProjType == 'public' orelse ProjType == 'private' -> + g_call(PidSpec, {read_latest_projection, ProjType}, Timeout). + read(PidSpec, ProjType, Epoch) -> read(PidSpec, ProjType, Epoch, infinity). read(PidSpec, ProjType, Epoch, Timeout) - when ProjType == 'public' orelse ProjType == 'private' -> + when ProjType == 'public' orelse ProjType == 'private', + is_integer(Epoch), Epoch >= 0 -> g_call(PidSpec, {read, ProjType, Epoch}, Timeout). write(PidSpec, ProjType, Proj) -> @@ -66,9 +77,35 @@ write(PidSpec, ProjType, Proj) -> write(PidSpec, ProjType, Proj, Timeout) when ProjType == 'public' orelse ProjType == 'private', - is_record(Proj, projection_v1) -> + is_record(Proj, projection_v1), + is_integer(Proj#projection_v1.epoch_number), + Proj#projection_v1.epoch_number >= 0 -> g_call(PidSpec, {write, ProjType, Proj}, Timeout). +get_all(PidSpec, ProjType) -> + get_all(PidSpec, ProjType, infinity). + +get_all(PidSpec, ProjType, Timeout) + when ProjType == 'public' orelse ProjType == 'private' -> + g_call(PidSpec, {get_all, ProjType}, Timeout). + +list_all(PidSpec, ProjType) -> + list_all(PidSpec, ProjType, infinity). + +list_all(PidSpec, ProjType, Timeout) + when ProjType == 'public' orelse ProjType == 'private' -> + g_call(PidSpec, {list_all, ProjType}, Timeout). + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +g_call(PidSpec, Arg, Timeout) -> + LC1 = lclock_get(), + {Res, LC2} = gen_server:call(PidSpec, {Arg, LC1}, Timeout), + lclock_update(LC2), + Res. + +%%%%%%%%%%%%%%%%%%%%%%%%%%% + init([DataDir, NotifyWedgeStateChanges]) -> lclock_init(), PublicDir = machi_util:make_projection_filename(DataDir, "public"), @@ -85,22 +122,19 @@ init([DataDir, NotifyWedgeStateChanges]) -> max_public_epoch=MaxPublicEpoch, max_private_epoch=MaxPrivateEpoch}}. -%%%%%%%%%%%%%%%%%%%%%%%%%%% - -g_call(PidSpec, Arg, Timeout) -> - LC1 = lclock_get(), - {Res, LC2} = gen_server:call(PidSpec, {Arg, LC1}, Timeout), - lclock_update(LC2), - Res. - -%%%%%%%%%%%%%%%%%%%%%%%%%%% - handle_call({{get_latest_epoch, ProjType}, LC1}, _From, S) -> LC2 = lclock_update(LC1), Epoch = if ProjType == public -> S#state.max_public_epoch; ProjType == private -> S#state.max_private_epoch end, {reply, {{ok, Epoch}, LC2}, S}; +handle_call({{read_latest_projection, ProjType}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + Epoch = if ProjType == public -> S#state.max_public_epoch; + ProjType == private -> S#state.max_private_epoch + end, + {Reply, NewS} = do_proj_read(ProjType, Epoch, S), + {reply, {Reply, LC2}, NewS}; handle_call({{read, ProjType, Epoch}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {Reply, NewS} = do_proj_read(ProjType, Epoch, S), @@ -109,6 +143,19 @@ handle_call({{write, ProjType, Proj}, LC1}, _From, S) -> LC2 = lclock_update(LC1), {Reply, NewS} = do_proj_write(ProjType, Proj, S), {reply, {Reply, LC2}, NewS}; +handle_call({{get_all, ProjType}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + Dir = pick_path(ProjType, S), + Epochs = find_all(Dir), + All = [begin + {{ok, Proj}, _} = do_proj_read(ProjType, Epoch, S), + Proj + end || Epoch <- Epochs], + {reply, {{ok, All}, LC2}, S}; +handle_call({{list_all, ProjType}, LC1}, _From, S) -> + LC2 = lclock_update(LC1), + Dir = pick_path(ProjType, S), + {reply, {{ok, find_all(Dir)}, LC2}, S}; handle_call(_Request, _From, S) -> Reply = whaaaaaaaaaaaaa, {reply, Reply, S}. @@ -127,6 +174,8 @@ code_change(_OldVsn, S, _Extra) -> %%%%%%%%%%%%%%%%%%%%%%%%%%% +do_proj_read(_ProjType, Epoch, S) when Epoch < 0 -> + {{error, not_written}, S}; do_proj_read(ProjType, Epoch, S) -> Dir = pick_path(ProjType, S), Path = filename:join(Dir, epoch2name(Epoch)), @@ -152,7 +201,16 @@ do_proj_write(ProjType, #projection_v1{epoch_number=Epoch}=Proj, S) -> ok = file:write(FH, term_to_binary(Proj)), ok = file:sync(FH), ok = file:close(FH), - {ok, S}; + NewS = if ProjType == public, Epoch > S#state.max_public_epoch -> + io:format(user, "TODO: tell ~p we are wedged by epoch ~p\n", [S#state.wedge_notify_pid, Epoch]), + S#state{max_public_epoch=Epoch, wedged=true}; + ProjType == private, Epoch > S#state.max_private_epoch -> + io:format(user, "TODO: tell ~p we are unwedged by epoch ~p\n", [S#state.wedge_notify_pid, Epoch]), + S#state{max_private_epoch=Epoch, wedged=false}; + true -> + S + end, + {ok, NewS}; {error, Else} -> {{error, Else}, S} end. @@ -168,6 +226,10 @@ epoch2name(Epoch) -> name2epoch(Name) -> machi_util:hexstr_to_int(Name). +find_all(Dir) -> + Fs = filelib:wildcard("*", Dir), + lists:sort([name2epoch(F) || F <- Fs]). + find_max_epoch(Dir) -> Fs = lists:sort(filelib:wildcard("*", Dir)), if Fs == [] -> diff --git a/test/machi_flu1_test.erl b/test/machi_flu1_test.erl index 3cd1daf..136d6d0 100644 --- a/test/machi_flu1_test.erl +++ b/test/machi_flu1_test.erl @@ -124,21 +124,22 @@ flu_projection_smoke_test() -> FLU1 = setup_test_flu(projection_test_flu, TcpPort, DataDir), try - {ok, -1} = ?FLU_C:get_latest_epoch(Host, TcpPort, public), - {ok, -1} = ?FLU_C:get_latest_epoch(Host, TcpPort, private), + [begin + {ok, -1} = ?FLU_C:get_latest_epoch(Host, TcpPort, T), + {error, not_written} = + ?FLU_C:read_latest_projection(Host, TcpPort, T), + {ok, []} = ?FLU_C:list_all(Host, TcpPort, T), + {ok, []} = ?FLU_C:get_all(Host, TcpPort, T), - P1 = machi_projection:new(1, a, [a], [], [a], [], []), - ok = ?FLU_C:write_projection(Host, TcpPort, public, P1), - {error, written} = ?FLU_C:write_projection(Host, TcpPort, public, P1), - {ok, P1} = ?FLU_C:read_projection(Host, TcpPort, public, 1), - {error, not_written} = ?FLU_C:read_projection(Host, TcpPort, public, 2), - - ok = ?FLU_C:write_projection(Host, TcpPort, private, P1), - {error, written} = ?FLU_C:write_projection(Host, TcpPort, private, P1), - {ok, P1} = ?FLU_C:read_projection(Host, TcpPort, private, 1), - {error, not_written} = ?FLU_C:read_projection(Host, TcpPort, private, 2), - - ok = ?FLU_C:quit(machi_util:connect(Host, TcpPort)) + P1 = machi_projection:new(1, a, [a], [], [a], [], []), + ok = ?FLU_C:write_projection(Host, TcpPort, T, P1), + {error, written} = ?FLU_C:write_projection(Host, TcpPort, T, P1), + {ok, P1} = ?FLU_C:read_projection(Host, TcpPort, T, 1), + {ok, P1} = ?FLU_C:read_latest_projection(Host, TcpPort, T), + {ok, [1]} = ?FLU_C:list_all(Host, TcpPort, T), + {ok, [P1]} = ?FLU_C:get_all(Host, TcpPort, T), + {error, not_written} = ?FLU_C:read_projection(Host, TcpPort, T, 2) + end || T <- [public, private] ] after ok = ?FLU:stop(FLU1) end.