Maxim Sokhatsky 11 years ago
parent
commit
32a2d90b3e

+ 10 - 0
.travis.yml

@@ -0,0 +1,10 @@
+language: erlang
+otp_release:
+  - R16B
+notifications:
+  email: 
+    - maxim@synrc.com
+    - doxtop@synrc.com
+  irc: "chat.freenode.net#n2o"
+install: "rebar get-deps compile"
+script: "rebar compile"

+ 306 - 0
README.md

@@ -0,0 +1,306 @@
+KVS: Erlang Abstract Term Database
+==================================
+
+Online Presentation: http://slid.es/maximsokhatsky/kvs
+
+Features
+--------
+
+* Polymorphic Tuples
+* Managing Linked-Lists
+* Various Backends Support: KAI, Mnesia, Riak, CouchDB
+* Sequential Consistency via Feed Server
+* Basic Schema for Social Sites and Accounting
+* Extendable Schema
+* Supports Secondary Indexes for KAI, Mnesia and Riak
+* Change Backends on-the-fly
+* Supports Multiple backends at the same time
+* Xen Ready
+
+Usage
+-----
+
+In rebar.config:
+
+    {kvs, ".*", {git, "git://github.com/synrc/kvs", "HEAD"}}
+
+Overview
+--------
+
+This is database handling application that hides database access
+and provides high-level rich API to stored and extend following data:
+
+KVS
+
+* Acl
+* Users
+* Subscriptions
+* Feeds
+* Entries
+* Comments
+
+KVS SOCIAL
+
+* Groups
+* Meetings
+* Accounts
+* Payments
+* Products
+* Purchases
+
+This Framework provides also a Feed Server Plugin for sequential consistency.
+All write requests with given object key will be handled by single processes
+in Feed Server so you may not worry about concurrent changes of user feed tops.
+
+All write operations that are made to data with secondary indexes,
+i.e. not like linked lists could be potentially handled without feed_server.
+But some KV storages are not supporting secondary indexes add those backends carefully.
+
+Store Backends
+--------------
+
+Currently kvs includes following store backends:
+
+* Mnesia
+* Riak
+* KAI
+
+Configuring
+-----------
+
+First of all you need to tune your backend in the kvs application:
+
+```erlang
+{kvs, {dba,store_kai}},
+```
+
+Try to check it:
+
+```erlang
+1> kvs:config(dba).
+store_kai
+
+2> kvs:version().
+{version,"KVS KAI PURE XEN"}
+```
+
+Create database for single node:
+
+```erlang
+3> kvs:join().
+[kvs] Mnesia Init
+ok
+4> kvs:init_db().
+[{"acl",ok},{"feed",ok}]
+```
+
+'init_db' output the creations of containers.
+You can also create database by joining to existing cluster:
+
+```erlang
+3> kvs:join('kvs@synrc.com').
+```
+
+In that case you don't need to do 'init_db'
+To check table packages included into the schema:
+
+```erlang
+4> kvs:dir().
+[{table,"id_seq"},
+ {table,"subscription"}, <- 2i
+ {table,"feed"}, <- feed
+ {table,"comment"},
+ {table,"entry"},
+ {table,"access"},
+ {table,"acl"}, <- feed
+ {table,"user"}]
+```
+
+Operations
+----------
+
+Try to add some data:
+
+```erlang
+1> rr(kvs).
+2> kvs:put(#user{id="maxim@synrc.com"}).
+ok
+3> kvs:get(user,"maxim@synrc.com").
+#user{id = "maxim@synrc.com",container = feed,...}
+4> kvs:put(#user{id="doxtop@synrc.com"}).
+5> length(kvs:all(user)).
+2
+```
+
+Polymorphic Records
+-------------------
+
+The data in KVS represented as plain Erlang records. The first element of the tuple
+as usual indicates the name of bucket. And the second element usually corresponds
+to the index key field. Additional secondary indexes could be applied for stores
+that supports 2i, e.g. kai, mnesia, riak.
+
+    1 record_name -- user, groups, acl, etc... table name -- element(1, Rec).
+    2 id          -- index key -- element(2, Rec).
+
+Iterators
+---------
+
+All record could be chained into the double-linked lists in the database.
+So you can inherit from the ITERATOR record just like that:
+
+```erlang
+-record(access, {?ITERATOR(acl),
+    entry_id,
+    acl_id,
+    accessor,
+    action}).
+```
+
+The layout of iterators are following:
+
+    1 record_name -- table name, like
+    2 id          -- index key
+    3 container   -- container name
+    4 feed_id     -- feed id
+    5 prev        -- poniter to previous object in list
+    6 next        -- next
+    7 feeds       -- subfeeds
+    8 guard,      -- aux field
+    9 ...
+
+This means your table will support add/remove linked list operations to lists.
+
+```erlang
+1> kvs:add(#user{id="mes@ua.fm"}).
+2> kvs:add(#user{id="dox@ua.fm"}).
+```
+
+Read the chain (undefined means all)
+
+```erlang
+3> kvs:entries(kvs:get(feed, user), user, undefined).
+[#user{id="mes@ua.fm"},#user{id="dox@ua.fm"}]
+```
+
+or just
+
+```erlang
+4> kvs:entries(user).
+[#user{id="mes@ua.fm"},#user{id="dox@ua.fm"}]
+```
+
+Read flat values by all keys from table:
+
+```erlang
+4> kvs:all(user).
+[#user{id="mes@ua.fm"},#user{id="dox@ua.fm"}]
+```
+
+Containers
+----------
+
+If you are using iterators records this automatically means you are using containers.
+Containers are just boxes for storing top/heads of the linked lists. Here is layout
+of containers:
+
+    1 record_name   -- container name
+    2 id            -- unique id
+    3 top           -- pointer to the list's head
+    4 entries_count -- number of elements in list
+
+Extending Schema
+----------------
+
+Usually you need only specify custom mnesia indexes and tables tuning.
+Riak and KAI backends don't need it. Group you table into table packages
+represented as modules with handle_notice API.
+
+```erlang
+-module(kvs_feed).
+-inclue_lib("kvs/include/kvs.hrl").
+
+metainfo() -> 
+    #schema{name=kvs,tables=[
+        #table{name=feed,container=true,fields=record_info(fields,feed)},
+        #table{ name=entry,container=feed,fields=record_info(fields,entry),
+                keys=[feed_id,entry_id,from]},
+        #table{name=comment,container=feed,fields=record_info(fields,comment),
+                keys=[entry_id,author_id]} ]}.
+```
+
+And plug it into schema config:
+
+```erlang
+{kvs, {schema,[kvs_user,kvs_acl,kvs_feed,kvs_subscription]}},
+```
+
+And on database init
+
+```erlang
+1> kvs:join().
+```
+
+It will create your custom schema.
+
+Business Logic
+--------------
+
+Here is Consumer behavior handlers of KVS FEEDS supervised processes
+
+```erlang
+handle_notice(  [kvs_feed,user,Owner,entry,Eid,add],
+                [#entry{feed_id=Fid}=Entry],
+                #state{feeds=Feeds}) ->
+
+                case lists:keyfind(Fid,2, S#state.feeds) of
+                    false -> skip;
+                    {_,_} -> add_entry(Eid,Fid,Entry) end,
+                {noreply, S};
+
+handle_notice(  [kvs_feed,user,Owner,entry,{Eid,FeedName},edit],
+                [#entry{feed_id=Fid}=Entry],
+                #state{feeds=Feeds}) ->
+
+                case lists:keyfind(FeedName,1,Feeds) of
+                    false -> skip;
+                    {_,Fid}-> update_entry(Eid,Fid,Entry) end,
+                {noreply, S};
+
+handle_notice(  [kvs_feed,user,Owner,entry,Eid,edit],
+                [#entry{feed_id=Fid}=Entry],
+                #state{feeds=Feeds}) ->
+
+                case lists:keyfind(Fid, 2, Feeds) of
+                    false -> skip;
+                    {_,_} -> update_entry(Eid,Fid,Entry) end,
+                {noreply, S};
+```
+
+Here is the private implementation
+
+```erlang
+add_entry(Eid,Fid,Entry) ->
+    E = Entry#entry{id = {Eid, Fid}, entry_id = Eid, feeds=[comments]},
+    Added = case kvs:add(E) of {error, Err} -> {error,Err}; {ok, En} -> En end,
+    msg:notify([kvs_feed, entry, {Eid, Fid}, added], [Added]).
+
+update_entry(Eid,Fid,Entry) -> ...
+```
+
+And that is how you can call it
+
+```erlang
+msg:notify([kvs_feed, user, "maxim@synrc.com", entry, Eid, add], [#entry{}]).
+```
+
+Credits
+-------
+
+* Maxim Sokhatsky
+* Andrii Zadorozhnii
+* Vladimir Kirillov
+* Alex Kalenuk
+* Sergey Polkovnikov
+
+OM A HUM

+ 9 - 0
include/acl.hrl

@@ -0,0 +1,9 @@
+-include("kvs.hrl").
+
+-record(acl, {?CONTAINER}).
+
+-record(access, {?ITERATOR(acl),
+        entry_id,
+        acl_id,
+        accessor,
+        action}).

+ 45 - 0
include/api.hrl

@@ -0,0 +1,45 @@
+-include("metainfo.hrl").
+
+% service
+
+-spec start() -> ok | {error,any()}.
+-spec stop() -> stopped.
+
+% schema change
+
+-spec destroy() -> ok.
+-spec join() -> ok | {error,any()}.
+-spec join(Node :: string()) -> [{atom(),any()}].
+-spec init_db() -> list(tuple(list(), skip | ok)).
+-spec init(Backend :: atom(), Module :: atom()) -> list(#table{}).
+
+% meta info
+
+-spec modules() -> list(atom()).
+-spec containers() -> list(tuple(atom(),list(atom()))).
+-spec tables() -> list(#table{}).
+-spec table(Tab :: atom()) -> #table{}.
+-spec version() -> {version,string()}.
+
+% chain ops
+
+-spec create(Container :: atom()) -> integer().
+-spec add(Record :: tuple()) -> {ok,tuple()} | {error,exist} | {error,no_container}.
+-spec remove(Record :: tuple()) -> ok | {error,any()}.
+-spec remove(Tab :: atom(), Key :: any()) -> ok | {error,any()}.
+
+% raw ops
+
+-spec put(Record :: tuple()) -> ok | {error,any()}.
+-spec delete(Tab :: atom(), Key :: any()) -> ok | {error,any()}.
+
+% read ops
+
+-spec get(Tab :: atom(), Key :: any()) -> {ok,any()} | {error,duplicated} | {error,not_found}.
+-spec get(Tab :: atom(), Key :: any(), Value :: any()) -> {ok,any()}.
+-spec index(Tab :: atom(), Key :: any(), Value :: any()) -> list(tuple()).
+
+% import/export
+
+-spec load_db(Path :: string()) -> list(ok | {error,any()}).
+-spec save_db(Path :: string()) -> ok | {error,any()}.

+ 10 - 0
include/comment.hrl

@@ -0,0 +1,10 @@
+-include("kvs.hrl").
+
+-record(comment, {?ITERATOR(feed), % {comment_id, entry_id, feed_id}
+        comment_id,
+        entry_id,
+        content,
+        from,
+        created,
+        media = [],
+        parent}).

+ 3 - 0
include/config.hrl

@@ -0,0 +1,3 @@
+-record(config, {key, value}).
+
+-define(DBA, kvs:config(dba)).

+ 16 - 0
include/entry.hrl

@@ -0,0 +1,16 @@
+-include("kvs.hrl").
+
+-record(entry, {?ITERATOR(feed), % {entry_id, feed_id}
+        entry_id,
+        from,
+        to,
+        title,
+        description,
+        created,
+        hidden,
+        access,
+        shared,
+        starred,
+        deleted,
+        media = [],
+        type = {user, normal}}).

+ 4 - 0
include/feed.hrl

@@ -0,0 +1,4 @@
+-include("kvs.hrl").
+
+-record(feed, {?CONTAINER, aclver}).
+

+ 11 - 0
include/group.hrl

@@ -0,0 +1,11 @@
+-include("kvs.hrl").
+
+-record(group,{?ITERATOR(feed, true),
+        name,
+        description,
+        scope :: public | private,
+        creator,
+        created,
+        owner,
+        users_count = 0 :: integer(),
+        entries_count = 0}).

+ 13 - 0
include/kvs.hrl

@@ -0,0 +1,13 @@
+-ifndef(KVS_HRL).
+-define(KVS_HRL, true).
+
+
+-define(CONTAINER, id, top=undefined, entries_count=0).
+-define(ITERATOR(Container, Guard), id, container=Container, feed_id, prev, next, feeds=[], guard=Guard, etc).
+-define(ITERATOR(Container), ?ITERATOR(Container, false)).
+
+-record(id_seq, {thing, id}).
+-record(container, {?CONTAINER}).
+-record(iterator,  {?ITERATOR(undefined)}).
+
+-endif.

+ 7 - 0
include/metainfo.hrl

@@ -0,0 +1,7 @@
+-ifndef(METAINFO_HRL).
+-define(METAINFO_HRL, true).
+
+-record(schema,{name,tables=[]}).
+-record(table,{name,container,fields=[],keys=[],copy_type=disc_copies}).
+
+-endif.

+ 22 - 0
include/product.hrl

@@ -0,0 +1,22 @@
+-include("kvs.hrl").
+
+-record(product, {?ITERATOR(feed, true),
+        ext_id                 :: term(),    % ext
+        vendor_id              :: integer(), % auto
+        categories             :: list(integer()), % admin
+        creator,
+        owner,
+        title,
+        brief,
+        cover,
+        publish_start_date     :: calendar:date_time(), % admin
+        publish_end_date       :: calendar:date_time(), % admin
+        price = 0              :: integer(),
+        currency               :: integer(),  % currency charge
+        retailer_price         :: integer(), % ext
+        our_price              :: integer(), % auto
+        fee                    :: integer(),  % net membership fee
+        enabled                :: boolean(), % admin
+        for_sale               :: boolean(),
+        created                :: calendar:date_time(), % auto
+        modify_date            :: calendar:date_time() }).

+ 7 - 0
include/state.hrl

@@ -0,0 +1,7 @@
+
+-record(state, {
+        owner = "feed_owner",
+        type :: user | group | system | product,
+        feeds = [],
+        callback,
+        cached_feeds=[]}).

+ 9 - 0
include/subscription.hrl

@@ -0,0 +1,9 @@
+
+-record(subscription,{
+        key,
+        who,
+        whom,
+        what,
+        how,
+        date,
+        note}).

+ 18 - 0
include/user.hrl

@@ -0,0 +1,18 @@
+-include("kvs.hrl").
+
+-record(user, {?ITERATOR(feed, true),
+        email,
+        username,
+        password,
+        display_name,
+        register_date,
+        tokens = [],
+        avatar,
+        names,
+        surnames,
+        birth,
+        sex,
+        date,
+        status,
+        zone,
+        type }).

+ 3 - 0
rebar.config

@@ -0,0 +1,3 @@
+{deps_dir, ["deps"]}.
+{deps,[
+]}.

+ 15 - 0
samples/Makefile

@@ -0,0 +1,15 @@
+RELEASE := kvs_shell
+COOKIE  := node_runner
+APPS    := mnesia kernel stdlib sasl kvs active
+VER     := 1.0.0
+PLT_NAME := ~/.kvs_dialyzer.plt
+VM      := rels/files/vm.args
+SYS     := rels/files/sys.config
+ERL_ARGS     := -args_file $(VM) -config $(SYS)
+RUN_DIR      ?= ./rels/devbox
+LOG_DIR      ?= ./rels/devbox/logs
+
+default: compile
+static-link:
+
+include otp.mk

+ 29 - 0
samples/depman.erl

@@ -0,0 +1,29 @@
+#!/usr/bin/env escript
+
+-module(depman).
+-compile([export_all]).
+-define(ABORT(Str, Args), io:format(Str, Args), throw(abort)).
+
+app_exists(App,Srv) when is_tuple(App) -> app_exists(element(1,App), Srv);
+app_exists(App,Srv) when is_atom(App) -> case reltool_server:get_app(Srv,App) of {ok, _} -> true; _ -> false end.
+
+validate_rel_apps(ReltoolServer, {sys, ReltoolConfig}) ->
+    case lists:keyfind(rel, 1, ReltoolConfig) of
+        false -> ok;
+        {rel, _Name, _Vsn, Apps} ->
+            Missing = lists:sort([App || App <- Apps, app_exists(App, ReltoolServer) == false]),
+            case Missing of [] -> ok; _ -> ?ABORT("Missing Apps: ~p\n", [Missing]) end;
+        Rel -> ?ABORT("Invalid {rel, ...} section in reltool.config: ~p\n", [Rel]) end.
+
+relconfig(Apps) ->
+    LibDirs = [Dir || Dir <- ["apps", "deps"], case file:read_file_info(Dir) of {ok, _} -> true; _ -> false end],
+    {sys, [ {lib_dirs,LibDirs}, {rel,"node","1",Apps}, {boot_rel,"node"}, {app,observer,[{incl_cond,exclude}]} ]}.
+
+main([]) -> ?ABORT("usage: ./depman.erl apps", []);
+main(MainApps) ->
+    Relconfig = relconfig([list_to_atom(A) || A <- MainApps]),
+    {ok, Server} = reltool:start_server([{config, Relconfig}]),
+    validate_rel_apps(Server, Relconfig),
+    {ok, {release, _Node, _Erts, Apps}} = reltool_server:get_rel(Server, "node"),
+    Alist = [element(1, A) || A <- Apps],
+    io:format("~w~n", [Alist]).

+ 42 - 0
samples/otp.mk

@@ -0,0 +1,42 @@
+empty :=
+ROOTS := apps deps
+space := $(empty) $(empty)
+comma := $(empty),$(empty)
+VSN   := $(shell expr substr `git rev-parse HEAD` 1 6)
+DATE  := $(shell git show -s --format="%ci" HEAD | sed -e 's/\+/Z/g' -e 's/-/./g' -e 's/ /-/g' -e 's/:/./g')
+ERL_LIBS := $(subst $(space),:,$(ROOTS))
+relx  := "{release,{$(RELEASE),\"$(VER)\"},[$(subst $(space),$(comma),$(APPS))]}.\\n{include_erts,true}.\
+\\n{extended_start_script,true}.\\n{generate_start_script,true}.\\n{sys_config,\"$(SYS)\"}.\
+\\n{vm_args,\"$(VM)\"}.\\n{overlay,[{mkdir,\"log/sasl\"}]}."
+
+test: eunit ct
+compile: get-deps
+delete-deps get-deps compile clean update-deps:
+	rebar $@
+.applist:
+	./depman.erl $(APPS) > $@
+$(RUN_DIR) $(LOG_DIR):
+	mkdir -p $(RUN_DIR) & mkdir -p $(LOG_DIR)
+console: .applist
+	ERL_LIBS=$(ERL_LIBS) erl $(ERL_ARGS) -eval \
+		'[ok = application:ensure_started(A, permanent) || A <- $(shell cat .applist)]'
+start: $(RUN_DIR) $(LOG_DIR) .applist
+	ERL_LIBS=$(ERL_LIBS) run_erl -daemon $(RUN_DIR)/ $(LOG_DIR)/ "exec $(MAKE) console"
+attach:
+	to_erl $(RUN_DIR)/
+release:
+	echo $(shell echo $(relx) > relx.config) & relx
+stop:
+	kill -9 `ps ax -o pid= -o command=|grep $(RELEASE)|grep $(COOKIE)|awk '{print $$1}'`
+$(PLT_NAME):
+	ERL_LIBS=deps dialyzer --build_plt --output_plt $(PLT_NAME) --apps $(APPS) || true
+dialyze: $(PLT_NAME) compile
+	dialyzer deps/*/ebin --plt $(PLT_NAME) --no_native -Werror_handling -Wunderspecs -Wrace_conditions
+tar:
+	tar zcvf $(RELEASE)-$(VSN)-$(DATE).tar.gz _rel/lib/*/ebin _rel/lib/*/priv _rel/bin _rel/releases
+eunit:
+	rebar eunit skip_deps=true
+ct:
+	rebar ct verbose=1
+
+.PHONY: delete-deps get-deps compile clean console start attach release update-deps dialyze ct eunit tar

+ 7 - 0
samples/rebar.config

@@ -0,0 +1,7 @@
+{sub_dirs,["deps","apps"]}.
+{lib_dirs,["apps"]}.
+{deps_dir,["deps"]}.
+{deps, [
+    {active,  ".*", {git, "git://github.com/synrc/active", {tag,"0.5"}}},
+    {kvs,     ".*", {git, "git://github.com/synrc/kvs", "HEAD"}}
+]}.

+ 34 - 0
samples/rels/files/erl

@@ -0,0 +1,34 @@
+#!/bin/sh
+
+## This script replaces the default "erl" in erts-VSN/bin. This is necessary
+## as escript depends on erl and in turn, erl depends on having access to a
+## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect
+## of running escript -- the embedded node bypasses erl and uses erlexec directly
+## (as it should).
+##
+## Note that this script makes the assumption that there is a start_clean.boot
+## file available in $ROOTDIR/release/VSN.
+
+# Determine the abspath of where this script is executing from.
+ERTS_BIN_DIR=$(cd ${0%/*} && pwd)
+
+# Now determine the root directory -- this script runs from erts-VSN/bin,
+# so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR
+# path.
+ROOTDIR=${ERTS_BIN_DIR%/*/*}
+
+# Parse out release and erts info
+START_ERL=`cat $ROOTDIR/releases/start_erl.data`
+ERTS_VSN=${START_ERL% *}
+APP_VSN=${START_ERL#* }
+
+BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
+EMU=beam
+PROGNAME=`echo $0 | sed 's/.*\\///'`
+CMD="$BINDIR/erlexec"
+export EMU
+export ROOTDIR
+export BINDIR
+export PROGNAME
+
+exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"}

+ 44 - 0
samples/rels/files/install_upgrade.escript

@@ -0,0 +1,44 @@
+#!/usr/bin/env escript
+%%! -noshell -noinput
+%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
+%% ex: ft=erlang ts=4 sw=4 et
+
+-define(TIMEOUT, 60000).
+-define(INFO(Fmt,Args), io:format(Fmt,Args)).
+
+main([NodeName, Cookie, ReleasePackage]) ->
+    TargetNode = start_distribution(NodeName, Cookie),
+    {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release,
+                         [ReleasePackage], ?TIMEOUT),
+    ?INFO("Unpacked Release ~p~n", [Vsn]),
+    {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler,
+                                    check_install_release, [Vsn], ?TIMEOUT),
+    {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler,
+                                    install_release, [Vsn], ?TIMEOUT),
+    ?INFO("Installed Release ~p~n", [Vsn]),
+    ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT),
+    ?INFO("Made Release ~p Permanent~n", [Vsn]);
+main(_) ->
+    init:stop(1).
+
+start_distribution(NodeName, Cookie) ->
+    MyNode = make_script_node(NodeName),
+    {ok, _Pid} = net_kernel:start([MyNode, shortnames]),
+    erlang:set_cookie(node(), list_to_atom(Cookie)),
+    TargetNode = make_target_node(NodeName),
+    case {net_kernel:hidden_connect_node(TargetNode),
+          net_adm:ping(TargetNode)} of
+        {true, pong} ->
+            ok;
+        {_, pang} ->
+            io:format("Node ~p not responding to pings.\n", [TargetNode]),
+            init:stop(1)
+    end,
+    TargetNode.
+
+make_target_node(Node) ->
+    [_, Host] = string:tokens(atom_to_list(node()), "@"),
+    list_to_atom(lists:concat([Node, "@", Host])).
+
+make_script_node(Node) ->
+    list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])).

+ 292 - 0
samples/rels/files/node

@@ -0,0 +1,292 @@
+#!/bin/sh
+# -*- tab-width:4;indent-tabs-mode:nil -*-
+# ex: ts=4 sw=4 et
+
+RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd)
+
+CALLER_DIR=$PWD
+
+RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*}
+RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc
+# Note the trailing slash on $PIPE_DIR/
+PIPE_DIR=/tmp/$RUNNER_BASE_DIR/
+RUNNER_USER=
+
+# Make sure this script is running as the appropriate user
+if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then
+    exec sudo -u $RUNNER_USER -i $0 $@
+fi
+
+# Identify the script name
+SCRIPT=`basename $0`
+
+# Parse out release and erts info
+START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data`
+ERTS_VSN=${START_ERL% *}
+APP_VSN=${START_ERL#* }
+
+# Use $CWD/vm.args if exists, otherwise releases/APP_VSN/vm.args, or else etc/vm.args
+if [ -e "$CALLER_DIR/vm.args" ]; then
+    VMARGS_PATH=$CALLER_DIR/vm.args
+    USE_DIR=$CALLER_DIR
+else
+    USE_DIR=$RUNNER_BASE_DIR
+    if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args" ]; then
+        VMARGS_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/vm.args"
+    else
+        VMARGS_PATH="$RUNNER_ETC_DIR/vm.args"
+    fi
+fi
+
+RUNNER_LOG_DIR=$USE_DIR/log
+# Make sure log directory exists
+mkdir -p $RUNNER_LOG_DIR
+
+# Use releases/VSN/sys.config if it exists otherwise use etc/app.config
+if [ -e "$USE_DIR/sys.config" ]; then
+    CONFIG_PATH="$USE_DIR/sys.config"
+else
+    if [ -e "$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config" ]; then
+        CONFIG_PATH="$RUNNER_BASE_DIR/releases/$APP_VSN/sys.config"
+    else
+        CONFIG_PATH="$RUNNER_ETC_DIR/app.config"
+    fi
+fi
+
+# Extract the target node name from node.args
+NAME_ARG=`egrep '^-s?name' $VMARGS_PATH`
+if [ -z "$NAME_ARG" ]; then
+    echo "vm.args needs to have either -name or -sname parameter."
+    exit 1
+fi
+
+# Extract the name type and name from the NAME_ARG for REMSH
+REMSH_TYPE=`echo $NAME_ARG | awk '{print $1}'`
+REMSH_NAME=`echo $NAME_ARG | awk '{print $2}'`
+
+# Note the `date +%s`, used to allow multiple remsh to the same node transparently
+REMSH_NAME_ARG="$REMSH_TYPE remsh`date +%s`@`echo $REMSH_NAME | awk -F@ '{print $2}'`"
+REMSH_REMSH_ARG="-remsh $REMSH_NAME"
+
+# Extract the target cookie
+COOKIE_ARG=`grep '^-setcookie' $VMARGS_PATH`
+if [ -z "$COOKIE_ARG" ]; then
+    echo "vm.args needs to have a -setcookie parameter."
+    exit 1
+fi
+
+# Make sure CWD is set to the right dir
+cd $USE_DIR
+
+# Make sure log directory exists
+mkdir -p $USE_DIR/log
+
+
+# Add ERTS bin dir to our path
+ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin
+
+# Setup command to control the node
+NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG"
+
+# Setup remote shell command to control node
+REMSH="$ERTS_PATH/erl $REMSH_NAME_ARG $REMSH_REMSH_ARG $COOKIE_ARG"
+
+# Check the first argument for instructions
+case "$1" in
+    start|start_boot)
+        # Make sure there is not already a node running
+        RES=`$NODETOOL ping`
+        if [ "$RES" = "pong" ]; then
+            echo "Node is already running!"
+            exit 1
+        fi
+        case "$1" in
+            start)
+                shift
+                START_OPTION="console"
+                HEART_OPTION="start"
+                ;;
+            start_boot)
+                shift
+                START_OPTION="console_boot"
+                HEART_OPTION="start_boot"
+                ;;
+        esac
+        RUN_PARAM=$(printf "\'%s\' " "$@")
+        HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT $HEART_OPTION $RUN_PARAM"
+        export HEART_COMMAND
+        mkdir -p $PIPE_DIR
+        $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT $START_OPTION $RUN_PARAM" 2>&1
+        ;;
+
+    stop)
+        # Wait for the node to completely stop...
+        case `uname -s` in
+            Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD)
+                # PID COMMAND
+                PID=`ps ax -o pid= -o command=|\
+                    grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'`
+                ;;
+            SunOS)
+                # PID COMMAND
+                PID=`ps -ef -o pid= -o args=|\
+                    grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'`
+                ;;
+            CYGWIN*)
+                # UID PID PPID TTY STIME COMMAND
+                PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'`
+                ;;
+        esac
+        $NODETOOL stop
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            exit $ES
+        fi
+        while `kill -0 $PID 2>/dev/null`;
+        do
+            sleep 1
+        done
+        ;;
+
+    restart)
+        ## Restart the VM without exiting the process
+        $NODETOOL restart
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            exit $ES
+        fi
+        ;;
+
+    reboot)
+        ## Restart the VM completely (uses heart to restart it)
+        $NODETOOL reboot
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            exit $ES
+        fi
+        ;;
+
+    ping)
+        ## See if the VM is alive
+        $NODETOOL ping
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            exit $ES
+        fi
+        ;;
+
+    attach)
+        # Make sure a node IS running
+        RES=`$NODETOOL ping`
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            echo "Node is not running!"
+            exit $ES
+        fi
+
+        shift
+        exec $ERTS_PATH/to_erl $PIPE_DIR
+        ;;
+
+    remote_console)
+        # Make sure a node IS running
+        RES=`$NODETOOL ping`
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            echo "Node is not running!"
+            exit $ES
+        fi
+
+        shift
+        exec $REMSH
+        ;;
+
+    upgrade)
+        if [ -z "$2" ]; then
+            echo "Missing upgrade package argument"
+            echo "Usage: $SCRIPT upgrade {package base name}"
+            echo "NOTE {package base name} MUST NOT include the .tar.gz suffix"
+            exit 1
+        fi
+
+        # Make sure a node IS running
+        RES=`$NODETOOL ping`
+        ES=$?
+        if [ "$ES" -ne 0 ]; then
+            echo "Node is not running!"
+            exit $ES
+        fi
+
+        node_name=`echo $NAME_ARG | awk '{print $2}'`
+        erlang_cookie=`echo $COOKIE_ARG | awk '{print $2}'`
+
+        $ERTS_PATH/escript $RUNNER_BASE_DIR/bin/install_upgrade.escript $node_name $erlang_cookie $2
+        ;;
+
+    console|console_clean|console_boot)
+        # .boot file typically just $SCRIPT (ie, the app name)
+        # however, for debugging, sometimes start_clean.boot is useful.
+        # For e.g. 'setup', one may even want to name another boot script.
+        case "$1" in
+            console)        BOOTFILE=$SCRIPT ;;
+            console_clean)  BOOTFILE=start_clean ;;
+            console_boot)
+                shift
+                BOOTFILE="$1"
+                shift
+                ;;
+        esac
+        # Setup beam-required vars
+        ROOTDIR=$RUNNER_BASE_DIR
+        BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
+        EMU=beam
+        PROGNAME=`echo $0 | sed 's/.*\\///'`
+        CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -mode embedded -config $CONFIG_PATH -args_file $VMARGS_PATH"
+        export EMU
+        export ROOTDIR
+        export BINDIR
+        export PROGNAME
+
+        # Dump environment info for logging purposes
+        echo "Exec: $CMD" -- ${1+"$@"}
+        echo "Root: $ROOTDIR"
+
+        # Log the startup
+        logger -t "$SCRIPT[$$]" "Starting up"
+
+        # Start the VM
+        exec $CMD -- ${1+"$@"}
+        ;;
+
+    foreground)
+        # start up the release in the foreground for use by runit
+        # or other supervision services
+
+        BOOTFILE=$SCRIPT
+        FOREGROUNDOPTIONS="-noinput +Bd"
+
+        # Setup beam-required vars
+        ROOTDIR=$RUNNER_BASE_DIR
+        BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
+        EMU=beam
+        PROGNAME=`echo $0 | sed 's/.*\///'`
+        CMD="$BINDIR/erlexec $FOREGROUNDOPTIONS -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -config $CONFIG_PATH -args_file $VMARGS_PATH"
+        export EMU
+        export ROOTDIR
+        export BINDIR
+        export PROGNAME
+
+        # Dump environment info for logging purposes
+        echo "Exec: $CMD" -- ${1+"$@"}
+        echo "Root: $ROOTDIR"
+
+        # Start the VM
+        exec $CMD -- ${1+"$@"}
+        ;;
+    *)
+        echo "Usage: $SCRIPT {start|start_boot <file>|foreground|stop|restart|reboot|ping|console|console_clean|console_boot <file>|attach|remote_console|upgrade}"
+        exit 1
+        ;;
+esac
+
+exit 0

+ 96 - 0
samples/rels/files/node.cmd

@@ -0,0 +1,96 @@
+@setlocal
+
+@set node_name=node
+
+@rem Get the absolute path to the parent directory,
+@rem which is assumed to be the node root.
+@for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI
+
+@set releases_dir=%node_root%\releases
+
+@rem Parse ERTS version and release version from start_erl.data
+@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @(
+    @call :set_trim erts_version %%I
+    @call :set_trim release_version %%J
+)
+
+@set vm_args=%releases_dir%\%release_version%\vm.args
+@set sys_config=%releases_dir%\%release_version%\sys.config
+@set node_boot_script=%releases_dir%\%release_version%\%node_name%
+@set clean_boot_script=%releases_dir%\%release_version%\start_clean
+
+@rem extract erlang cookie from vm.args
+@for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie "%vm_args%"`) do @set erlang_cookie=%%J
+
+@set erts_bin=%node_root%\erts-%erts_version%\bin
+
+@set service_name=%node_name%_%release_version%
+
+@set erlsrv="%erts_bin%\erlsrv.exe"
+@set epmd="%erts_bin%\epmd.exe"
+@set escript="%erts_bin%\escript.exe"
+@set werl="%erts_bin%\werl.exe"
+
+@if "%1"=="usage" @goto usage
+@if "%1"=="install" @goto install
+@if "%1"=="uninstall" @goto uninstall
+@if "%1"=="start" @goto start
+@if "%1"=="stop" @goto stop
+@if "%1"=="restart" @call :stop && @goto start
+@if "%1"=="console" @goto console
+@if "%1"=="query" @goto query
+@if "%1"=="attach" @goto attach
+@if "%1"=="upgrade" @goto upgrade
+@echo Unknown command: "%1"
+
+:usage
+@echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade]
+@goto :EOF
+
+:install
+@set description=Erlang node %node_name% in %node_root%
+@set start_erl=%node_root%\bin\start_erl.cmd
+@set args= ++ %node_name% ++ %node_root%
+@%erlsrv% add %service_name% -c "%description%" -sname %node_name% -w "%node_root%" -m "%start_erl%" -args "%args%" -stopaction "init:stop()."
+@goto :EOF
+
+:uninstall
+@%erlsrv% remove %service_name%
+@%epmd% -kill
+@goto :EOF
+
+:start
+@%erlsrv% start %service_name%
+@goto :EOF
+
+:stop
+@%erlsrv% stop %service_name%
+@goto :EOF
+
+:console
+@start "%node_name% console" %werl% -boot "%node_boot_script%" -config "%sys_config%" -args_file "%vm_args%" -sname %node_name%
+@goto :EOF
+
+:query
+@%erlsrv% list %service_name%
+@exit %ERRORLEVEL%
+@goto :EOF
+
+:attach
+@for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I
+start "%node_name% attach" %werl% -boot "%clean_boot_script%" -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie%
+@goto :EOF
+
+:upgrade
+@if "%2"=="" (
+    @echo Missing upgrade package argument
+    @echo Usage: %~n0 upgrade {package base name}
+    @echo NOTE {package base name} MUST NOT include the .tar.gz suffix
+    @goto :EOF
+)
+@%escript% %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2
+@goto :EOF
+
+:set_trim
+@set %1=%2
+@goto :EOF

+ 138 - 0
samples/rels/files/nodetool

@@ -0,0 +1,138 @@
+%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
+%% ex: ft=erlang ts=4 sw=4 et
+%% -------------------------------------------------------------------
+%%
+%% nodetool: Helper Script for interacting with live nodes
+%%
+%% -------------------------------------------------------------------
+
+main(Args) ->
+    ok = start_epmd(),
+    %% Extract the args
+    {RestArgs, TargetNode} = process_args(Args, [], undefined),
+
+    %% See if the node is currently running  -- if it's not, we'll bail
+    case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
+        {true, pong} ->
+            ok;
+        {_, pang} ->
+            io:format("Node ~p not responding to pings.\n", [TargetNode]),
+            halt(1)
+    end,
+
+    case RestArgs of
+        ["ping"] ->
+            %% If we got this far, the node already responsed to a ping, so just dump
+            %% a "pong"
+            io:format("pong\n");
+        ["stop"] ->
+            io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]);
+        ["restart"] ->
+            io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]);
+        ["reboot"] ->
+            io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]);
+        ["rpc", Module, Function | RpcArgs] ->
+            case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
+                          [RpcArgs], 60000) of
+                ok ->
+                    ok;
+                {badrpc, Reason} ->
+                    io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
+                    halt(1);
+                _ ->
+                    halt(1)
+            end;
+        ["rpcterms", Module, Function, ArgsAsString] ->
+            case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
+                          consult(ArgsAsString), 60000) of
+                {badrpc, Reason} ->
+                    io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
+                    halt(1);
+                Other ->
+                    io:format("~p\n", [Other])
+            end;
+        Other ->
+            io:format("Other: ~p\n", [Other]),
+            io:format("Usage: nodetool {ping|stop|restart|reboot}\n")
+    end,
+    net_kernel:stop().
+
+process_args([], Acc, TargetNode) ->
+    {lists:reverse(Acc), TargetNode};
+process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) ->
+    erlang:set_cookie(node(), list_to_atom(Cookie)),
+    process_args(Rest, Acc, TargetNode);
+process_args(["-name", TargetName | Rest], Acc, _) ->
+    ThisNode = append_node_suffix(TargetName, "_maint_"),
+    {ok, _} = net_kernel:start([ThisNode, longnames]),
+    process_args(Rest, Acc, nodename(TargetName));
+process_args(["-sname", TargetName | Rest], Acc, _) ->
+    ThisNode = append_node_suffix(TargetName, "_maint_"),
+    {ok, _} = net_kernel:start([ThisNode, shortnames]),
+    process_args(Rest, Acc, nodename(TargetName));
+process_args([Arg | Rest], Acc, Opts) ->
+    process_args(Rest, [Arg | Acc], Opts).
+
+
+start_epmd() ->
+    [] = os:cmd(epmd_path() ++ " -daemon"),
+    ok.
+
+epmd_path() ->
+    ErtsBinDir = filename:dirname(escript:script_name()),
+    Name = "epmd",
+    case os:find_executable(Name, ErtsBinDir) of
+        false ->
+            case os:find_executable(Name) of
+                false ->
+                    io:format("Could not find epmd.~n"),
+                    halt(1);
+                GlobalEpmd ->
+                    GlobalEpmd
+            end;
+        Epmd ->
+            Epmd
+    end.
+
+
+nodename(Name) ->
+    case string:tokens(Name, "@") of
+        [_Node, _Host] ->
+            list_to_atom(Name);
+        [Node] ->
+            [_, Host] = string:tokens(atom_to_list(node()), "@"),
+            list_to_atom(lists:concat([Node, "@", Host]))
+    end.
+
+append_node_suffix(Name, Suffix) ->
+    case string:tokens(Name, "@") of
+        [Node, Host] ->
+            list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host]));
+        [Node] ->
+            list_to_atom(lists:concat([Node, Suffix, os:getpid()]))
+    end.
+
+
+%%
+%% Given a string or binary, parse it into a list of terms, ala file:consult/0
+%%
+consult(Str) when is_list(Str) ->
+    consult([], Str, []);
+consult(Bin) when is_binary(Bin)->
+    consult([], binary_to_list(Bin), []).
+
+consult(Cont, Str, Acc) ->
+    case erl_scan:tokens(Cont, Str, 0) of
+        {done, Result, Remaining} ->
+            case Result of
+                {ok, Tokens, _} ->
+                    {ok, Term} = erl_parse:parse_term(Tokens),
+                    consult([], Remaining, [Term | Acc]);
+                {eof, _Other} ->
+                    lists:reverse(Acc);
+                {error, Info, _} ->
+                    {error, Info}
+            end;
+        {more, Cont1} ->
+            consult(Cont1, eof, Acc)
+    end.

+ 40 - 0
samples/rels/files/start_erl.cmd

@@ -0,0 +1,40 @@
+@setlocal
+
+@rem Parse arguments. erlsrv.exe prepends erl arguments prior to first ++.
+@rem Other args are position dependent.
+@set args="%*"
+@for /F "delims=++ tokens=1,2,3" %%I in (%args%) do @(
+    @set erl_args=%%I
+    @call :set_trim node_name %%J
+    @rem Trim spaces from the left of %%K (node_root), which may have spaces inside
+    @for /f "tokens=* delims= " %%a in ("%%K") do @set node_root=%%a
+)
+
+@set releases_dir=%node_root%\releases
+
+@rem parse ERTS version and release version from start_erl.dat
+@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @(
+    @call :set_trim erts_version %%I
+    @call :set_trim release_version %%J
+)
+
+@set erl_exe="%node_root%\erts-%erts_version%\bin\erl.exe"
+@set boot_file="%releases_dir%\%release_version%\%node_name%"
+
+@if exist "%releases_dir%\%release_version%\sys.config" (
+    @set app_config="%releases_dir%\%release_version%\sys.config"
+) else (
+    @set app_config="%node_root%\etc\app.config"
+)
+
+@if exist "%releases_dir%\%release_version%\vm.args" (
+    @set vm_args="%releases_dir%\%release_version%\vm.args"
+) else (
+    @set vm_args="%node_root%\etc\vm.args"
+)
+
+@%erl_exe% %erl_args% -boot %boot_file% -config %app_config% -args_file %vm_args%
+
+:set_trim
+@set %1=%2
+@goto :EOF

+ 15 - 0
samples/rels/files/sys.config

@@ -0,0 +1,15 @@
+[
+ {kvs,
+     [{pass_init_db,true},
+      {nodes,[]},
+      {dba, store_mnesia},
+      {schema, [kvs_user, kvs_acl, kvs_feed, kvs_subscription]}]},
+ {sasl, [
+         {sasl_error_logger, {file, "log/sasl-error.log"}},
+         {errlog_type, error},
+         {error_logger_mf_dir, "log/sasl"},      % Log directory
+         {error_logger_mf_maxbytes, 10485760},   % 10 MB max file size
+         {error_logger_mf_maxfiles, 5}           % 5 files max
+        ]}
+].
+

+ 6 - 0
samples/rels/files/vm.args

@@ -0,0 +1,6 @@
+-name kvs@127.0.0.1
+-setcookie node_runner
++K true
++A 5
+-env ERL_MAX_PORTS 4096
+-env ERL_FULLSWEEP_AFTER 10

+ 40 - 0
samples/rels/reltool.config

@@ -0,0 +1,40 @@
+{sys, [
+       {lib_dirs, ["../..","../deps"]},
+       {erts, [{mod_cond, derived}, {app_file, strip}]},
+       {app_file, strip},
+       {rel, "node", "1",
+        [
+         kernel,
+         stdlib,
+         sasl,
+         kvs
+        ]},
+       {rel, "start_clean", "",
+        [
+         kernel,
+         stdlib
+        ]},
+       {boot_rel, "node"},
+       {profile, embedded},
+       {incl_cond, derived},
+       {mod_cond, derived},
+       {excl_archive_filters, [".*"]}, %% Do not archive built libs
+       {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)",
+                           "^erts.*/(doc|info|include|lib|man|src)"]},
+       {excl_app_filters, ["\.gitignore"]},
+       {app, hipe, [{mod_cond, app}, {incl_cond, exclude}]}
+      ]}.
+
+{target_dir, "node"}.
+
+{overlay, [
+           {mkdir, "log/sasl"},
+           {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"},
+           {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"},
+           {copy, "files/node", "bin/node"},
+           {copy, "files/node.cmd", "bin/node.cmd"},
+           {copy, "files/start_erl.cmd", "bin/start_erl.cmd"},
+           {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"},
+           {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"},
+           {copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"}
+          ]}.

+ 7 - 0
src/kvs.app.src

@@ -0,0 +1,7 @@
+{application, kvs,
+   [{description, "KVS Abstract Term Database"},
+    {vsn, "1"},
+    {registered, []},
+    {applications, [kernel,stdlib,mnesia]},
+    {mod, { kvs_app, []}},
+    {env, []} ]}.

+ 284 - 0
src/kvs.erl

@@ -0,0 +1,284 @@
+-module(kvs).
+-copyright('Synrc Research Center s.r.o.').
+-compile(export_all).
+
+-include("api.hrl").
+-include("config.hrl").
+-include("metainfo.hrl").
+-include("state.hrl").
+-include("kvs.hrl").
+-include_lib("stdlib/include/qlc.hrl").
+
+% NOTE: API Documentation
+
+-export([start/0,stop/0]).                                        % service
+-export([destroy/0,join/0,join/1,init_db/0,init/2]).              % schema change
+-export([modules/0,containers/0,tables/0,table/1,version/0]).     % meta info
+-export([create/1,add/1,remove/2,remove/1]).                      % chain ops
+-export([put/1,delete/2,next_id/2]).                              % raw ops
+-export([get/2,get/3,index/3]).                                   % read ops
+-export([load_db/1,save_db/1]).                                   % import/export
+
+start() -> DBA = ?DBA, DBA:start().
+stop() -> DBA = ?DBA, DBA:stop().
+
+destroy() -> DBA = ?DBA, DBA:destroy().
+join() -> DBA = ?DBA, DBA:join().
+join(Node) -> DBA = ?DBA, DBA:join(Node).
+init(Backend, Module) ->
+    [ begin
+        Backend:create_table(T#table.name, [{attributes,T#table.fields},{T#table.copy_type, [node()]}]),
+        [ Backend:add_table_index(T#table.name, Key) || Key <- T#table.keys ],
+        T
+    end || T <- (Module:metainfo())#schema.tables ].
+
+version() -> DBA=?DBA, DBA:version().
+tables() -> lists:flatten([ (M:metainfo())#schema.tables || M <- modules() ]).
+table(Name) -> lists:keyfind(Name,#table.name,tables()).
+dir() -> DBA = ?DBA, DBA:dir().
+modules() -> kvs:config(schema).
+containers() ->
+    lists:flatten([ [ {T#table.name,T#table.fields}
+        || T=#table{container=true} <- (M:metainfo())#schema.tables ]
+    || M <- modules() ]).
+
+create(ContainerName) -> create(ContainerName, kvs:next_id(atom_to_list(ContainerName), 1)).
+
+create(ContainerName, Id) ->
+    wf:info("kvs:create: ~p",[ContainerName]),
+    Instance = list_to_tuple([ContainerName|proplists:get_value(ContainerName, kvs:containers())]),
+    Top = setelement(#container.id,Instance,Id),
+    Top2 = setelement(#container.top,Top,undefined),
+    Top3 = setelement(#container.entries_count,Top2,0),
+    ok = kvs:put(Top3),
+    Id.
+
+add(Record) when is_tuple(Record) ->
+
+    Id = element(#iterator.id, Record),
+
+    case kvs:get(element(1,Record), Id) of
+        {error, not_found} ->
+
+            Type = element(1, Record),
+            CName = element(#iterator.container, Record),
+            Cid = case element(#iterator.feed_id, Record) of
+                undefined -> element(1,Record);
+                Fid -> Fid end,
+
+            Container = case kvs:get(CName, Cid) of
+                {ok,C} -> C;
+                {error, not_found} when Cid /= undefined ->
+
+                    NC = setelement(#container.id,
+                            list_to_tuple([CName|proplists:get_value(CName, kvs:containers())]), Cid),
+                    NC1 = setelement(#container.entries_count, NC, 0),
+
+                    kvs:put(NC1),NC1;
+
+                _ -> error end,
+
+            if  Container == error -> {error, no_container};
+                true ->
+
+                    Next = undefined,
+                    Prev = case element(#container.top, Container) of
+                        undefined -> undefined;
+                        Tid -> case kvs:get(Type, Tid) of
+                            {error, not_found} -> undefined;
+                            {ok, Top} ->
+                                NewTop = setelement(#iterator.next, Top, Id),
+                                kvs:put(NewTop),
+                                element(#iterator.id, NewTop) end end,
+
+                    C1 = setelement(#container.top, Container, Id),
+                    C2 = setelement(#container.entries_count, C1,
+                            element(#container.entries_count, Container)+1),
+
+                    kvs:put(C2),
+
+                    R  = setelement(#iterator.feeds, Record,
+                            [ case F1 of
+                                {FN, Fd} -> {FN, Fd};
+                                _-> {F1, kvs:create(CName,{F1,element(#iterator.id,Record)})}
+                              end || F1 <- element(#iterator.feeds, Record)]),
+
+                    R1 = setelement(#iterator.next,    R,  Next),
+                    R2 = setelement(#iterator.prev,    R1, Prev),
+                    R3 = setelement(#iterator.feed_id, R2, element(#container.id, Container)),
+
+                    kvs:put(R3),
+
+                    kvs:info(?MODULE,"[kvs] put: ~p~n", [element(#container.id,R3)]),
+
+                    {ok, R3}
+            end;
+        {ok, _} -> kvs:info(?MODULE,"[kvs] entry exist while put: ~p~n", [Id]), {error, exist} end.
+
+remove(RecordName, RecordId) ->
+    case kvs:get(RecordName, RecordId) of
+        {error, not_found} -> ksv:error("[kvs] can't remove ~p~n",[{RecordName,RecordId}]);
+        {ok, E} ->
+
+            Id = element(#iterator.id, E),
+            CName = element(#iterator.container, E),
+            Cid = element(#iterator.feed_id, E),
+
+            {ok, Container} = kvs:get(CName, Cid),
+            Top = element(#container.top, Container),
+
+            Next = element(#iterator.next, E),
+            Prev = element(#iterator.prev, E),
+
+            case kvs:get(RecordName, Next) of
+                {ok, NE} ->
+                    NewNext = setelement(#iterator.prev, NE, Prev),
+                    kvs:put(NewNext);
+                    _ -> ok end,
+
+            case kvs:get(RecordName, Prev) of
+                {ok, PE} ->
+                    NewPrev = setelement(#iterator.next, PE, Next),
+                    kvs:put(NewPrev);
+                    _ -> ok end,
+
+            C1 = case Top of Id -> setelement(#container.top, Container, Prev); _ -> Container end,
+            C2 = setelement(#container.entries_count, C1, element(#container.entries_count, Container)-1),
+
+            kvs:put(C2),
+
+            kvs:info(?MODULE,"[kvs] delete: ~p id: ~p~n", [RecordName, Id]),
+
+            kvs:delete(RecordName, Id) end.
+
+remove(E) when is_tuple(E) ->
+
+    Id    = element(#iterator.id, E),
+    CName = element(#iterator.container, E),
+    Cid   = element(#iterator.feed_id, E),
+
+    {ok, Container} = kvs:get(CName, Cid),
+
+    Top   = element(#container.top, Container),
+    Next  = element(#iterator.next, E),
+    Prev  = element(#iterator.prev, E),
+
+    case kvs:get(element(1,E), Next) of
+        {ok, NE} ->
+            NewNext = setelement(#iterator.prev, NE, Prev),
+            kvs:put(NewNext); _ -> ok end,
+
+    case kvs:get(element(1,E), Prev) of
+        {ok, PE} ->
+            NewPrev = setelement(#iterator.next, PE, Next),
+            kvs:put(NewPrev);
+        _ -> ok end,
+
+    C1 = case Top of Id -> setelement(#container.top, Container, Prev); _ -> Container end,
+    C2 = setelement(#container.entries_count, C1, element(#container.entries_count, Container)-1),
+
+    kvs:put(C2),
+
+    kvs:info(?MODULE,"[kvs] delete: ~p", [Id]),
+
+    kvs:delete(E).
+
+traversal( _,undefined,_,_) -> [];
+traversal(_,_,0,_) -> [];
+traversal(RecordType, Start, Count, Direction)->
+    case kvs:get(RecordType, Start) of {error,_} -> [];
+    {ok, R} ->  Prev = element(Direction, R),
+                Count1 = case Count of C when is_integer(C) -> C - 1; _-> Count end,
+                [R | traversal(RecordType, Prev, Count1, Direction)] end.
+
+entries(Name) -> Table = kvs:table(Name), entries(kvs:get(Table#table.container,Name), Name, undefined).
+entries(Name, Count) -> Table = kvs:table(Name), entries(kvs:get(Table#table.container,Name), Name, Count).
+entries({ok, Container}, RecordType, Count) -> entries(Container, RecordType, Count);
+entries(Container, RecordType, Count) when is_tuple(Container) ->
+    traversal(RecordType, element(#container.top, Container), Count, #iterator.prev).
+
+entries(RecordType, Start, Count, Direction) ->
+    E = traversal(RecordType, Start, Count, Direction),
+    case Direction of #iterator.next -> lists:reverse(E); #iterator.prev -> E end.
+
+init_db() ->
+    case kvs:get(id_seq,"feed") of
+        {error,_} -> add_seq_ids();
+        {ok,_} -> skip end.
+
+add_seq_ids() ->
+    Init = fun(Key) ->
+           case kvs:get(id_seq, Key) of
+                {error, _} -> {Key,kvs:put(#id_seq{thing = Key, id = 0})};
+                {ok, _} -> {Key,skip} end end,
+    [ Init(atom_to_list(Name))  || {Name,_Fields} <- containers() ].
+
+
+put(Record) ->
+    DBA=?DBA,
+    DBA:put(Record).
+
+get(RecordName, Key) ->
+    DBA=?DBA,
+    DBA:get(RecordName, Key).
+
+get(RecordName, Key, Default) ->
+    DBA=?DBA,
+    case DBA:get(RecordName, Key) of
+        {ok,{RecordName,Key,Value}} ->
+            kvs:info(?MODULE,"[kvs] get config value: ~p~n", [{RecordName, Key, Value}]),
+            {ok,Value};
+        {error, _B} ->
+            kvs:info(?MODULE,"[kvs] new config value: ~p~n", [{RecordName, Key, Default}]),
+            DBA:put({RecordName,Key,Default}),
+            {ok,Default} end.
+
+delete(Tab, Key) -> DBA=?DBA,DBA:delete(Tab, Key).
+count(RecordName) -> DBA=?DBA,DBA:count(RecordName).
+all(RecordName) -> DBA=?DBA,DBA:all(RecordName).
+index(RecordName, Key, Value) -> DBA=?DBA,DBA:index(RecordName, Key, Value).
+next_id(RecordName, Incr) -> DBA=?DBA,DBA:next_id(RecordName, Incr).
+
+save_db(Path) ->
+    Data = lists:append([all(B) || B <- [list_to_atom(Name) || {table,Name} <- kvs:dir()] ]),
+    kvs:save(Path, Data).
+
+load_db(Path) ->
+    add_seq_ids(),
+    AllEntries = kvs:load(Path),
+    [kvs:put(E) || E <- lists:filter(fun(E) -> is_tuple(E) end ,AllEntries)].
+
+save(Dir, Value) ->
+    filelib:ensure_dir(Dir),
+    file:write_file(Dir, term_to_binary(Value)).
+
+load(Key) ->
+    {ok, Bin} = file:read_file(Key),
+    binary_to_term(Bin).
+
+config(Key) -> config(kvs, Key, "").
+config(App,Key) -> config(App,Key, "").
+config(App, Key, Default) -> case application:get_env(App,Key) of
+                              undefined -> Default;
+                              {ok,V} -> V end.
+
+log_modules() -> [].
+-define(ALLOWED, (config(kvs,log_modules,kvs))).
+
+log(Module, String, Args, Fun) ->
+    case lists:member(Module,?ALLOWED:log_modules()) of
+         true -> error_logger:Fun(String, Args);
+         false -> skip end.
+
+info(Module,String, Args) ->  log(Module,String, Args, info_msg).
+info(String, Args) -> log(?MODULE, String, Args, info_msg).
+info(String) -> log(?MODULE, String, [], info_msg).
+
+warning(Module,String, Args) -> log(Module, String, Args, warning_msg).
+warning(String, Args) -> log(?MODULE, String, Args, warning_msg).
+warning(String) -> log(?MODULE,String, [], warning_msg).
+
+error(Module,String, Args) -> log(Module, String, Args, error_msg).
+error(String, Args) -> log(?MODULE, String, Args, error_msg).
+error(String) -> log(?MODULE, String, [], error_msg).
+

+ 37 - 0
src/kvs_acl.erl

@@ -0,0 +1,37 @@
+-module(kvs_acl).
+-copyright('Synrc Research Center s.r.o.').
+-compile(export_all).
+-include("kvs.hrl").
+-include("metainfo.hrl").
+-include("acl.hrl").
+-include("user.hrl").
+-include("group.hrl").
+-include("feed.hrl").
+
+metainfo() -> 
+    #schema{name=kvs,tables=[
+        #table{name=acl,container=true,fields=record_info(fields,acl)},
+        #table{name=access,container=acl,fields=record_info(fields,access)}
+    ]}.
+
+define_access(Accessor, Resource, Action) -> 
+    Entry = #access{ id={Accessor, Resource}, accessor=Accessor, action=Action},
+    case kvs:add(Entry) of
+        {error, exist} -> kvs:put(Entry#access{action=Action});
+        {error, no_container} -> skip;
+        {ok, E} -> E end.
+
+check(Keys) ->
+    Acls = [Acl || {ok, Acl = #access{}} <- [kvs:get(access, Key) || Key <- Keys]],
+    case Acls of
+        [] -> none;
+        [#access{action = Action} | _] -> Action end.
+
+check_access(#user{id = Id}, Feature) ->
+    Query = [ {{user,Id},Feature} ],
+    check(Query);
+
+check_access(Id, Feature) ->
+    case kvs:get(user, Id) of
+        {ok, User} -> check_access(User, Feature);
+        E -> E end.

+ 6 - 0
src/kvs_app.erl

@@ -0,0 +1,6 @@
+-module(kvs_app).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) -> kvs_sup:start_link().
+stop(_State) -> ok.

+ 122 - 0
src/kvs_feed.erl

@@ -0,0 +1,122 @@
+-module(kvs_feed).
+-copyright('Synrc Research Center, s.r.o.').
+-compile(export_all).
+-include("entry.hrl").
+-include("feed.hrl").
+-include("metainfo.hrl").
+-include("comment.hrl").
+-include("state.hrl").
+
+metainfo() -> 
+    #schema{name=kvs,tables=[
+        #table{name=entry,container=feed,fields=record_info(fields,entry),keys=[feed_id,entry_id,from]},
+        #table{name=comment,container=feed,fields=record_info(fields,comment),keys=[entry_id,author_id]},
+        #table{name=feed,container=true,fields=record_info(fields,feed)}
+    ]}.
+
+comments_count(entry, Eid) -> case kvs:get(entry, Eid) of {error,_} -> 0; {ok, E} -> comments_count([E],0) end;
+comments_count(product, Pid)->case kvs:get(product, Pid) of {error,_}->0; {ok, P} -> comments_count([P], 0) end;
+comments_count([], Acc) -> Acc;
+comments_count([E|T], Acc) ->
+    C = case lists:keyfind(comments, 1, element(#iterator.feeds, E)) of false -> 0;
+    {_, Fid} -> case kvs:get(feed, Fid) of {error,_} -> 0;
+        {ok, Feed } -> Feed#feed.entries_count 
+            + comments_count(kvs:entries(Feed, comment, undefined), 0) end end,
+    comments_count(T,  C + Acc).
+
+author_comments(Who) ->
+    EIDs = [E || #comment{entry_id=E} <- kvs:index(comment,from, Who) ],
+    lists:flatten([ kvs:index(entry, id,EID) || EID <- EIDs]).
+
+%% MQ API
+
+handle_notice(  [kvs_feed,_,Owner,entry,{Eid,Fid},add],
+                [#entry{feed_id=Fid}=Entry],
+                #state{owner=Owner}=State) ->
+
+                case lists:keyfind(Fid,2, State#state.feeds) of
+                    false -> skip;
+                    {_,_} -> add_entry({Eid,Fid},Entry) end,
+
+                {noreply, State};
+
+handle_notice(  [kvs_feed,_,_,comment,Cid,add],
+                [#comment{id={Cid,{_,Fid}, _}}=Comment],
+                #state{feeds=Feeds}=State) ->
+
+                case lists:keyfind(Fid,2,Feeds) of
+                    false -> skip;
+                    {_,_}-> add_comment(Comment) end,
+
+                {noreply, State};
+
+handle_notice(  [kvs_feed,_,Owner,entry,{Eid,Fid},edit],
+                [#entry{feed_id=Fid}=Entry],
+                #state{owner=Owner, feeds=Feeds}=State) ->
+
+                case lists:keyfind(Fid,1,Feeds) of
+                    false -> skip;
+                    {_,Fid}-> update_entry({Eid,Fid},Entry) end,
+
+                {noreply, State};
+
+handle_notice(  [kvs_feed,_,entry,delete],
+                [#entry{id=Id,feed_id=Fid}=Entry],
+                #state{feeds=Feeds}=State) ->
+
+                kvs:info(?MODULE,"[kvs_feed] delete entry ~p",[Id]),
+                case lists:keyfind(Fid,2,Feeds) of
+                    false -> ok;
+                    _ -> kvs:info(?MODULE,"[kvs_feed] => Remove entry ~p from feed ~p", [Id, Fid]),
+                         kvs:remove(entry, Id),
+                         msg:notify([kvs_feed, entry, Id, deleted], [Entry]) end,
+
+                {noreply,State};
+
+handle_notice(  [kvs_feed,Owner,delete],
+                [#entry{entry_id=Eid}=Entry],
+                #state{owner=Owner}=State) ->
+
+                kvs:info(?MODULE,"[kvs_feed] delete all entries ~p ~p", [Entry#entry.entry_id, Owner]),
+
+                [ msg:notify([kvs_feed,To,entry,delete],[Ed])
+                    || #entry{to={_, To}}=Ed <- kvs:index(entry, entry_id, Eid) ],
+
+                Fid = element(1,Entry),
+                kvs:remove(entry,{Eid, Fid}),
+                Removed = Entry#entry{id={Eid,Fid},feed_id=Fid},
+                msg:notify([kvs_feed,entry,{Eid, Fid},deleted], [Removed]),
+
+                {noreply, State};
+
+handle_notice(_Route, _Message, State) -> {noreply, State}.
+
+notify([Module|_]=Path, Message, State) ->
+    case kvs:config(feeds) of
+        enabled -> msg:notify(Path,Message);
+        _ -> Module:handle_notice(Path,Message,State) end.
+
+add_comment(Comment=#comment{}) ->
+    kvs:info(?MODULE,"[kvs_feed] add comment: ~p to feed ~p", [Comment#comment.id, Comment#comment.feed_id]),
+    C = Comment#comment{feeds=[comments]},
+    Added = case kvs:add(C) of {error, E} -> {error, E}; {ok, Cm} -> Cm end,
+    msg:notify([kvs_feed, comment, C#comment.id, added], [Added]).
+
+add_entry({Eid,Fid},Entry=#entry{}) ->
+    kvs:info(?MODULE,"[kvs_feed] add entry: ~p to feed ~p.", [Entry#entry.id, Entry#entry.feed_id]),
+    E = Entry#entry{id = {Eid, Fid}, entry_id = Eid, feeds=[comments]},
+    Added = case kvs:add(E) of {error, Err}-> {error,Err}; {ok, En} -> En end,
+    msg:notify([kvs_feed, entry, E#entry.id, added], [Added]).
+
+update_entry(Key={Eid,Fid},Entry) ->
+    case kvs:get(entry,Key) of
+        {error,_} -> skip;
+        {ok, E} ->
+            kvs:info(?MODULE,"[kvs_feed] update entry ~p in feed ~p", [Eid,Fid]),
+            Updated = E#entry{description=Entry#entry.description,
+                      title = Entry#entry.title,
+                      media = Entry#entry.media,
+                      etc   = Entry#entry.etc,
+                      type  = Entry#entry.type},
+            kvs:put(Updated),
+            msg:notify([kvs_feed,entry,Key,updated], [Updated]) end.

+ 26 - 0
src/kvs_subscription.erl

@@ -0,0 +1,26 @@
+-module(kvs_subscription).
+-copyright('Synrc Research Center s.r.o.').
+-include("subscription.hrl").
+-include("user.hrl").
+-include("metainfo.hrl").
+-compile(export_all).
+
+metainfo() ->
+    #schema{name=kvs,tables=[
+        #table{name=subscription,fields=record_info(fields,subscription),keys=[whom,who]},
+        #table{name=id_seq,fields=record_info(fields,id_seq),keys=[thing]}
+    ]}.
+
+subscribe(Who, Whom) -> kvs:put(#subscription{key={Who,Whom},who = Who, whom = Whom}).
+unsubscribe(Who, Whom) ->
+    case subscribed(Who, Whom) of
+        true  -> kvs:delete(subscription, {Who, Whom});
+        false -> skip end.
+
+subscriptions(#user{username = UId}) -> subscriptions(UId);
+subscriptions(UId) -> kvs:index(subscription, who, UId).
+subscribed(Who) -> kvs:index(subscription, whom, Who).
+subscribed(Who, Whom) ->
+    case kvs:get(subscription, {Who, Whom}) of
+        {ok, _} -> true;
+        _ -> false end.

+ 13 - 0
src/kvs_sup.erl

@@ -0,0 +1,13 @@
+-module(kvs_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+
+    kvs:start(),
+
+    {ok, { {one_for_one, 5, 10}, []} }.

+ 25 - 0
src/kvs_user.erl

@@ -0,0 +1,25 @@
+-module(kvs_user).
+-copyright('Synrc Research Center s.r.o.').
+-include("user.hrl").
+-include("config.hrl").
+-include("state.hrl").
+-include("metainfo.hrl").
+-compile(export_all).
+
+metainfo() -> 
+    #schema{name=kvs,tables=[
+        #table{name=user,container=feed,fields=record_info(fields,user),
+                 keys=[facebook_id,googleplus_id,twitter_id,github_id]}
+    ]}.
+
+handle_notice([kvs_user, user, registered], {_,_,#user{id=Who}=U}, #state{owner=Who}=State)->
+    kvs:info(?MODULE,"[kvs_user] process registration: ~p", [U]),
+    {noreply, State};
+
+handle_notice([kvs_user, user, Owner, delete], [#user{}=U], #state{owner=Owner}=State) ->
+    kvs:info(?MODULE,"[kvs_user] delete user: ~p", [U]),
+    {noreply, State};
+
+handle_notice(_Route, _Message, State) -> 
+    kvs:info(?MODULE,"[kvs_user] unknown notice."),
+    {noreply, State}.

+ 52 - 0
src/store_mnesia.erl

@@ -0,0 +1,52 @@
+-module(store_mnesia).
+-copyright('Synrc Research Center s.r.o.').
+-include("config.hrl").
+-include("metainfo.hrl").
+-include_lib("stdlib/include/qlc.hrl").
+-compile(export_all).
+
+start()    -> mnesia:start().
+stop()     -> mnesia:stop().
+destroy()  -> [mnesia:delete_table(list_to_atom(T))||{_,T}<-kvs:dir()], mnesia:delete_schema([node()]), ok.
+version()  -> {version,"KVS MNESIA"}.
+dir()      -> [{table,atom_to_list(T)}||T<-mnesia:system_info(local_tables)].
+join()     -> mnesia:change_table_copy_type(schema, node(), disc_copies), initialize().
+join(Node) ->
+    mnesia:change_config(extra_db_nodes, [Node]),
+    mnesia:change_table_copy_type(schema, node(), disc_copies),
+    [{Tb, mnesia:add_table_copy(Tb, node(), Type)}
+     || {Tb, [{N, Type}]} <- [{T, mnesia:table_info(T, where_to_commit)}
+                               || T <- mnesia:system_info(tables)], Node==N].
+
+initialize() ->
+    kvs:info(?MODULE,"[store_mnesia] mnesia init.~n"),
+    mnesia:create_schema([node()]),
+    [ kvs:init(store_mnesia,Module) || Module <- kvs:modules() ],
+    mnesia:wait_for_tables([ T#table.name || T <- kvs:tables()],5000).
+
+index(Tab,Key,Value) ->
+    Table = kvs:table(Tab),
+    Index = string:str(Table#table.fields,[Key]),
+    lists:flatten(many(fun() -> mnesia:index_read(Tab,Value,Index+1) end)).
+
+get(RecordName, Key) -> just_one(fun() -> mnesia:read(RecordName, Key) end).
+put(Records) when is_list(Records) -> void(fun() -> lists:foreach(fun mnesia:write/1, Records) end);
+put(Record) -> put([Record]).
+delete(Tab, Key) ->
+    case mnesia:transaction(fun()-> mnesia:delete({Tab, Key}) end) of
+        {aborted,Reason} -> {error,Reason};
+        {atomic,_Result} -> ok end.
+count(RecordName) -> mnesia:table_info(RecordName, size).
+all(R) -> lists:flatten(many(fun() -> L= mnesia:all_keys(R), [ mnesia:read({R, G}) || G <- L ] end)).
+next_id(RecordName, Incr) -> mnesia:dirty_update_counter({id_seq, RecordName}, Incr).
+many(Fun) -> case mnesia:transaction(Fun) of {atomic, R} -> R; _ -> [] end.
+void(Fun) -> case mnesia:transaction(Fun) of {atomic, ok} -> ok; {aborted, Error} -> {error, Error} end.
+create_table(Name,Options) -> mnesia:create_table(Name, Options).
+add_table_index(Record, Field) -> mnesia:add_table_index(Record, Field).
+exec(Q) -> F = fun() -> qlc:e(Q) end, {atomic, Val} = mnesia:transaction(F), Val.
+just_one(Fun) ->
+    case mnesia:transaction(Fun) of
+        {atomic, []} -> {error, not_found};
+        {atomic, [R]} -> {ok, R};
+        {atomic, [_|_]} -> {error, duplicated};
+        Error -> Error end.

+ 45 - 0
test/kvs_SUITE.erl

@@ -0,0 +1,45 @@
+-module(kvs_SUITE).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("kvs/include/entry.hrl").
+-compile(export_all).
+
+suite() -> [{timetrap,{seconds,30}}].
+all() -> [{group, feed},{group,acl}].
+groups() -> [{feed,[entry,comment,user]},
+             {acl,[access]}].
+
+init_per_suite(Config) ->
+    application:start(mnesia),
+    application:start(kvs),
+    application:set_env(kvs, schema, [kvs_user, kvs_acl, kvs_feed, kvs_subscription]),
+    application:set_env(kvs, dba, store_mnesia),
+    kvs:join(),
+    kvs:init_db(),
+    ct:log("-> Dir ~p~n",[kvs:dir()]),
+    Config.
+
+end_per_suite(Config) ->
+    kvs:destroy(),
+    application:stop(kvs),
+    ok.
+
+init_per_group(_Name, _Config) ->
+    ok.
+end_per_group(_Name, _Config) ->
+    ok.
+
+access(Config) -> ok.
+comment(Config) -> ok.
+user(Config) -> ok.
+entry(Config) ->
+    Fid = 1,
+    kvs:add(#entry{id={1,Fid},feed_id=Fid}),
+    kvs:add(#entry{id={2,Fid},feed_id=Fid}),
+    L = kvs:entries(kvs:get(feed,Fid),entry,undefined),
+    List = [ Key || #entry{id=Key} <- L ],
+    Length = length(List),
+    2 == Length,
+    List == [{1,1},{2,1}],
+    ct:log("-> List ~p~n", [List]),
+    ok.