From 43126fa8e84c17d5c82b36a5951dbfd6fe8cf2c7 Mon Sep 17 00:00:00 2001 From: Henry Sun Date: Fri, 12 Jul 2024 12:04:57 -0700 Subject: [PATCH] Update README and kvstore examples Summary: as title Differential Revision: D59690854 fbshipit-source-id: 64289c6b560ea9e79ec2ee93b5511216bbfb96d7 --- README.md | 106 ++++++++++++++++-------- examples/kvstore/src/kvstore.app.src | 10 ++- examples/kvstore/src/kvstore_app.erl | 8 +- examples/kvstore/src/kvstore_client.erl | 25 +++--- examples/kvstore/src/kvstore_sup.erl | 21 ++--- 5 files changed, 106 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 90ab59b..05739f4 100644 --- a/README.md +++ b/README.md @@ -11,24 +11,61 @@ WARaft is a Raft library in Erlang by WhatsApp. It provides an Erlang implementa ## Get Started -The following code snippet gives a quick glance about how WARaft works. It creates single node WARaft cluster and write a record. - -The [example directory](https://github.com/WhatsApp/waraft/tree/main/examples/kvstore/src) contains a generic key-value store built on top of WARaft. +The following code snippet gives a quick glance about how WARaft works. It creates a single-node WARaft cluster and writes and reads a record. ``` -% Cluster config - single node. table name test, partition 1 -1> Spec = wa_raft_sup:child_spec([#{table => test, partition => 1, nodes => [node()]}]). +% Setup the WARaft application and the host application +rr(wa_raft_server). +application:ensure_all_started(wa_raft). +application:set_env(test_app, raft_database, "."). +% Create a spec for partition 1 of the RAFT table "test" and start it. +Spec = wa_raft_sup:child_spec(test_app, [#{table => test, partition => 1}]). +% Here we add WARaft to the kernel's supervisor, but you should place WARaft's +% child spec underneath your application's supervisor in a real deployment. +supervisor:start_child(kernel_sup, Spec). +% Check that the RAFT server started successfully +wa_raft_server:status(raft_server_test_1). +% Make a cluster configuration with the current node as the only member +Config = wa_raft_server:make_config([#raft_identity{name = raft_server_test_1, node = node()}]). +% Bootstrap the RAFT server by force-promoting it +wa_raft_server:promote(raft_server_test_1, 1, true, Config). +% Check that now the RAFT server is the leader +wa_raft_server:status(raft_server_test_1). +% Read and write against a key +wa_raft_acceptor:commit(raft_acceptor_test_1, {make_ref(), {write, test, key, 1000}}). +wa_raft_acceptor:read(raft_acceptor_test_1, {read, test, key}). +``` -% Start raft processes under kernel_sup as supervisor. It's for demo purpose only. An app supervisor should be used for a real case -2> supervisor:start_child(kernel_sup, Spec). -{ok,<0.140.0>} +A typical output would look like the following: -% Check raft server status -3> wa_raft_server:status(raft_server_test_1). +``` +1> % Setup the WARaft application and the host application + rr(wa_raft_server). +[raft_application,raft_identifier,raft_identity,raft_log, + raft_log_pos,raft_options,raft_state] +2> application:ensure_all_started(wa_raft). +{ok,[wa_raft]} +3> application:set_env(test_app, raft_database, "."). +ok +4> % Create a spec for partition 1 of the RAFT table "test" and start it. + Spec = wa_raft_sup:child_spec(test_app, [#{table => test, partition => 1}]). +#{id => wa_raft_sup,restart => permanent,shutdown => infinity, + start => + {wa_raft_sup,start_link, + [test_app,[#{table => test,partition => 1}],#{}]}, + type => supervisor, + modules => [wa_raft_sup]} +5> % Here we add WARaft to the kernel's supervisor, but you should place WARaft's + % child spec underneath your application's supervisor in a real deployment. + supervisor:start_child(kernel_sup, Spec). +{ok,<0.103.0>} +6> % Check that the RAFT server started successfully + wa_raft_server:status(raft_server_test_1). [{state,stalled}, {id,nonode@nohost}, + {table,test}, {partition,1}, - {data_dir,"missing/test.1/"}, + {data_dir,"./test.1"}, {current_term,0}, {voted_for,undefined}, {commit_index,0}, @@ -42,17 +79,23 @@ The [example directory](https://github.com/WhatsApp/waraft/tree/main/examples/kv {votes,#{}}, {inflight_applies,0}, {disable_reason,undefined}, - {config,#{version => 1}}] - -% Promote current node as leader -4> wa_raft_server:promote(raft_server_test_1, 1, true, #{version => 1, membership => [{raft_server_test_1, node()}]}). + {config,#{version => 1}}, + {config_index,0}, + {witness,false}] +7> % Make a cluster configuration with the current node as the only member + Config = wa_raft_server:make_config([#raft_identity{name = raft_server_test_1, node = node()}]). +#{version => 1, + membership => [{raft_server_test_1,nonode@nohost}]} +8> % Bootstrap the RAFT server by force-promoting it + wa_raft_server:promote(raft_server_test_1, 1, true, Config). ok - -5> wa_raft_server:status(raft_server_test_1). -[{state,leader}, % leader node +9> % Check that now the RAFT server is the leader + wa_raft_server:status(raft_server_test_1). +[{state,leader}, {id,nonode@nohost}, + {table,test}, {partition,1}, - {data_dir,"missing/test.1/"}, + {data_dir,"./test.1"}, {current_term,1}, {voted_for,undefined}, {commit_index,1}, @@ -66,24 +109,19 @@ ok {votes,#{}}, {inflight_applies,0}, {disable_reason,undefined}, - {config,#{membership => [{raft_server_test_1,nonode@nohost}], - version => 1}}] - -% Write {key, 1000} to raft_server_test_1 -6> wa_raft_acceptor:commit(raft_acceptor_test_1, {make_ref(), {write, test, key, 1000}}). -{ok, 2} - -% Read key -7> wa_raft_acceptor:commit(raft_acceptor_test_1, {make_ref(), {read, test, key}}). -{ok,{1000,#{},2}} - -% Stop raft -8> supervisor:terminate_child(kernel_sup, wa_raft_sup). -ok -9> supervisor:delete_child(kernel_sup, wa_raft_sup). + {config,#{version => 1, + membership => [{raft_server_test_1,nonode@nohost}]}}, + {config_index,1}, + {witness,false}] +10> % Read and write against a key + wa_raft_acceptor:commit(raft_acceptor_test_1, {make_ref(), {write, test, key, 1000}}). ok +11> wa_raft_acceptor:read(raft_acceptor_test_1, {read, test, key}). +{ok,1000} ``` +The [example directory](https://github.com/WhatsApp/waraft/tree/main/examples/kvstore/src) contains an example generic key-value store built on top of WARaft. + ## License WARaft is [Apache licensed](./LICENSE). diff --git a/examples/kvstore/src/kvstore.app.src b/examples/kvstore/src/kvstore.app.src index e4c444b..fe07c3b 100644 --- a/examples/kvstore/src/kvstore.app.src +++ b/examples/kvstore/src/kvstore.app.src @@ -15,6 +15,14 @@ stdlib, wa_raft ]}, - {env, []}, + {env, [ + % Specify where you want your data to be stored here + {raft_database, "/mnt/kvstore"}, + % Specify your own implementations here + {raft_log_module, wa_raft_log_ets}, + {raft_storage_module, wa_raft_storage_ets}, + {raft_distribution_module, wa_raft_distribution}, + {raft_transport_module, wa_raft_transport} + ]}, {mod, {kvstore_app, []}} ]}. diff --git a/examples/kvstore/src/kvstore_app.erl b/examples/kvstore/src/kvstore_app.erl index 9aae848..882dbce 100644 --- a/examples/kvstore/src/kvstore_app.erl +++ b/examples/kvstore/src/kvstore_app.erl @@ -6,16 +6,18 @@ -module(kvstore_app). -compile(warn_missing_spec_all). +-behaviour(application). + %% API -export([ start/2, stop/1 ]). --spec start(application:start_type(), term()) -> {ok, pid()} | {ok, pid(), State :: term()} | {error, Reason :: term()}. +-spec start(application:start_type(), term()) -> {ok, pid()}. start(normal, _Args) -> - kvstore_sup:start_link(). + {ok, _Pid} = kvstore_sup:start_link(). --spec stop(State) -> ok when State :: term(). +-spec stop(term()) -> ok. stop(_State) -> ok. diff --git a/examples/kvstore/src/kvstore_client.erl b/examples/kvstore/src/kvstore_client.erl index c6e6ccd..c6db785 100644 --- a/examples/kvstore/src/kvstore_client.erl +++ b/examples/kvstore/src/kvstore_client.erl @@ -15,34 +15,33 @@ delete/1 ]). --include_lib("kernel/include/logger.hrl"). +-include_lib("wa_raft/include/wa_raft.hrl"). -define(CALL_TIMEOUT, 5000). -define(TABLE, kvstore). -define(NUM_PARTITIONS, 4). --define(PARTITION(P), list_to_atom(lists:concat(["raft_acceptor_", ?TABLE, "_" , P]))). %% Read value for a given key. It's a blocking call. --spec read(term()) -> {ok, {term(), map(), number()}} | {error, term()}. +-spec read(term()) -> {ok, term()} | wa_raft_acceptor:read_error(). read(Key) -> - execute(Key, {read, ?TABLE, Key}). + Acceptor = ?RAFT_ACCEPTOR_NAME(?TABLE, partition(Key)), + wa_raft_acceptor:read(Acceptor, {read, ?TABLE, Key}, ?CALL_TIMEOUT). %% Write a key/value pair to storage. It's a blocking call. --spec write(term(), term()) -> {ok, number()} | {error, term()}. +-spec write(term(), term()) -> ok | wa_raft_acceptor:commit_error(). write(Key, Value) -> - execute(Key, {write, ?TABLE, Key, Value}). + commit(Key, {write, ?TABLE, Key, Value}). %% Delete a key/value pair. It's a blocking call. --spec delete(term()) -> ok | {error, term()}. +-spec delete(term()) -> ok | wa_raft_acceptor:commit_error(). delete(Key) -> - execute(Key, {delete, ?TABLE, Key}). + commit(Key, {delete, ?TABLE, Key}). --spec execute(term(), term()) -> term(). -execute(Key, Command) -> - Partition = ?PARTITION(partition(Key)), - gen_server:call(Partition, {commit, {make_ref(), Command}}, ?CALL_TIMEOUT). +-spec commit(term(), term()) -> term() | wa_raft_acceptor:commit_error(). +commit(Key, Command) -> + Acceptor = ?RAFT_ACCEPTOR_NAME(?TABLE, partition(Key)), + wa_raft_acceptor:commit(Acceptor, {make_ref(), Command}, ?CALL_TIMEOUT). -spec partition(term()) -> number(). partition(Key) -> erlang:phash2(Key, ?NUM_PARTITIONS) + 1. - diff --git a/examples/kvstore/src/kvstore_sup.erl b/examples/kvstore/src/kvstore_sup.erl index 8d9f43c..b3f0a39 100644 --- a/examples/kvstore/src/kvstore_sup.erl +++ b/examples/kvstore/src/kvstore_sup.erl @@ -25,18 +25,13 @@ start_link() -> init([]) -> Partitions = [1, 2, 3, 4], Args = [raft_args(P) || P <- Partitions], - ChildSpecs = wa_raft_sup:child_spec(Args), - {ok, {#{}, [ChildSpecs]}}. + ChildSpecs = [ + wa_raft_sup:child_spec(Args) + ], + {ok, {#{}, ChildSpecs}}. -%% Return raft arguments for the provided partition --spec raft_args(wa_raft:partition()) -> wa_raft:args(). +% Construct a RAFT "args" for a partition. +-spec raft_args(Partition :: wa_raft:partition()) -> wa_raft:args(). raft_args(Partition) -> - #{ - %% Table name and partition uniquely identify a RAFT partition - table => kvstore, - partition => Partition, - %% Use in-memory log (Implement your own to meet your needs) - log_module => wa_raft_log_ets, - %% Use in-memory local storage (Implement your own to meet your needs) - storage_module => wa_raft_storage_ets - }. + % RAFT clusters are primarily identified by their table and partition number + #{table => kvstore, partition => Partition}.