Merge branch 'master' into 1278-add-clustered-db-info
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 7761007..5610779 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -320,11 +320,15 @@
 db_req(#httpd{method='GET',path_parts=[DbName]}=Req, _Db) ->
     % measure the time required to generate the etag, see if it's worth it
     T0 = os:timestamp(),
-    {ok, DbInfo} = fabric:get_db_info(DbName),
+    {ok, DbInfo} = fabric:get_db_info(DbName, [{format, aggregate}]),
     DeltaT = timer:now_diff(os:timestamp(), T0) / 1000,
     couch_stats:update_histogram([couchdb, dbinfo], DeltaT),
     send_json(Req, {DbInfo});
 
+db_req(#httpd{method='GET',path_parts=[DbName, <<"_info">>]}=Req, _Db) ->
+    {ok, DbInfo} = fabric:get_db_info(DbName, [{format, set}]),
+    send_json(Req, {DbInfo});
+
 db_req(#httpd{method='POST', path_parts=[DbName], user_ctx=Ctx}=Req, Db) ->
     chttpd:validate_ctype(Req, "application/json"),
 
diff --git a/src/fabric/src/fabric.erl b/src/fabric/src/fabric.erl
index 4a07271..5c517a3 100644
--- a/src/fabric/src/fabric.erl
+++ b/src/fabric/src/fabric.erl
@@ -18,9 +18,9 @@
 
 % DBs
 -export([all_dbs/0, all_dbs/1, create_db/1, create_db/2, delete_db/1,
-    delete_db/2, get_db_info/1, get_doc_count/1, set_revs_limit/3,
-    set_security/2, set_security/3, get_revs_limit/1, get_security/1,
-    get_security/2, get_all_security/1, get_all_security/2,
+    delete_db/2, get_db_info/1, get_db_info/2, get_doc_count/1,
+    set_revs_limit/3, set_security/2, set_security/3, get_revs_limit/1,
+    get_security/1, get_security/2, get_all_security/1, get_all_security/2,
     compact/1, compact/2]).
 
 % Documents
@@ -44,6 +44,7 @@
 -type callback() :: fun((any(), any()) -> {ok | stop, any()}).
 -type json_obj() :: {[{binary() | atom(), any()}]}.
 -type option() :: atom() | {atom(), any()}.
+-type db_info_type() :: set | aggregate.
 
 %% db operations
 %% @equiv all_dbs(<<>>)
@@ -84,6 +85,23 @@
 get_db_info(DbName) ->
     fabric_db_info:go(dbname(DbName)).
 
+%% @doc returns a property list of interesting properties
+%%      about the database such as `doc_count', `disk_size',
+%%      etc.
+%%      TODO: fix return type def
+-spec get_db_info(dbname(), db_info_type()) ->
+    {ok, [
+        {instance_start_time, binary()} |
+        {doc_count, non_neg_integer()} |
+        {doc_del_count, non_neg_integer()} |
+        {purge_seq, non_neg_integer()} |
+        {compact_running, boolean()} |
+        {disk_size, non_neg_integer()} |
+        {disk_format_version, pos_integer()}
+    ]}.
+get_db_info(DbName, Options) ->
+    fabric_db_info:go(dbname(DbName), Options).
+
 %% @doc the number of docs in a database
 -spec get_doc_count(dbname()) ->
     {ok, non_neg_integer()} |
diff --git a/src/fabric/src/fabric_db_info.erl b/src/fabric/src/fabric_db_info.erl
index 98e8e52..1518034 100644
--- a/src/fabric/src/fabric_db_info.erl
+++ b/src/fabric/src/fabric_db_info.erl
@@ -12,21 +12,28 @@
 
 -module(fabric_db_info).
 
--export([go/1]).
+-export([go/1, go/2]).
 
 -include_lib("fabric/include/fabric.hrl").
 -include_lib("mem3/include/mem3.hrl").
 
 go(DbName) ->
+    go(DbName, [{format, aggregate}]).
+
+go(DbName, Options) ->
     Shards = mem3:shards(DbName),
     Workers = fabric_util:submit_jobs(Shards, get_db_info, []),
     RexiMon = fabric_util:create_monitors(Shards),
-    Fun = fun handle_message/3,
+    Fun = case couch_util:get_value(format, Options, aggregate) of
+        set -> fun handle_set_message/3;
+        _ -> fun handle_aggr_message/3
+    end,
     {ok, ClusterInfo} = get_cluster_info(Shards),
-    Acc0 = {fabric_dict:init(Workers, nil), [{cluster, ClusterInfo}]},
+    Acc0 = fabric_dict:init(Workers, nil),
     try
         case fabric_util:recv(Workers, #shard.ref, Fun, Acc0) of
-            {ok, Acc} -> {ok, Acc};
+            {ok, Acc} ->
+                {ok, [{cluster, {ClusterInfo}} | Acc]};
             {timeout, {WorkersDict, _}} ->
                 DefunctWorkers = fabric_util:remove_done_workers(
                     WorkersDict,
@@ -37,51 +44,84 @@
                     "get_db_info"
                 ),
                 {error, timeout};
-            {error, Error} -> throw(Error)
+            {error, Error} ->
+                throw(Error)
         end
     after
         rexi_monitor:stop(RexiMon)
     end.
 
-handle_message({rexi_DOWN, _, {_,NodeRef},_}, _Shard, {Counters, Acc}) ->
-    case fabric_util:remove_down_workers(Counters, NodeRef) of
-    {ok, NewCounters} ->
-        {ok, {NewCounters, Acc}};
-    error ->
-        {error, {nodedown, <<"progress not possible">>}}
+
+handle_aggr_message({ok, Info}, Shard, Counters) ->
+    case fabric_dict:lookup_element(Shard, Counters) of
+        undefined ->
+            % already heard from someone else in this range
+            {ok, Counters};
+        nil ->
+            C1 = fabric_dict:store(Shard, Info, Counters),
+            C2 = fabric_view:remove_overlapping_shards(Shard, C1),
+            case fabric_dict:any(nil, C2) of
+                true ->
+                    {ok, C2};
+                false ->
+                    {stop, merge_results(C2)}
+            end
     end;
 
-handle_message({rexi_EXIT, Reason}, Shard, {Counters, Acc}) ->
+handle_aggr_message({rexi_DOWN, _, {_,NodeRef},_}, _Shard, Counters) ->
+    case fabric_util:remove_down_workers(Counters, NodeRef) of
+        {ok, NewCounters} ->
+            {ok, NewCounters};
+        error ->
+            {error, {nodedown, <<"progress not possible">>}}
+    end;
+
+handle_aggr_message({rexi_EXIT, Reason}, Shard, Counters) ->
     NewCounters = fabric_dict:erase(Shard, Counters),
     case fabric_view:is_progress_possible(NewCounters) of
-    true ->
-        {ok, {NewCounters, Acc}};
-    false ->
-        {error, Reason}
+        true ->
+            {ok, NewCounters};
+        false ->
+            {error, Reason}
     end;
 
-handle_message({ok, Info}, #shard{dbname=Name} = Shard, {Counters, Acc}) ->
-    case fabric_dict:lookup_element(Shard, Counters) of
-    undefined ->
-        % already heard from someone else in this range
-        {ok, {Counters, Acc}};
-    nil ->
-        Seq = couch_util:get_value(update_seq, Info),
-        C1 = fabric_dict:store(Shard, Seq, Counters),
-        C2 = fabric_view:remove_overlapping_shards(Shard, C1),
-        case fabric_dict:any(nil, C2) of
+handle_aggr_message(Else, _Shard, _Acc) ->
+    %% TODO: do we want this behavior change?
+    {error, Else}.
+
+
+handle_set_message({ok, Info}, Shard, Counters) ->
+    C1 = fabric_dict:store(Shard, Info, Counters),
+    case fabric_dict:any(nil, C1) of
         true ->
-            {ok, {C2, [Info|Acc]}};
+            {ok, C1};
         false ->
-            {stop, [
-                {db_name,Name},
-                {update_seq, fabric_view_changes:pack_seqs(C2)} |
-                merge_results(lists:flatten([Info|Acc]))
-            ]}
-        end
+            {stop, [{shards, {format_results(C1)}}]}
     end;
-handle_message(_, _, Acc) ->
-    {ok, Acc}.
+
+handle_set_message({rexi_DOWN, _, {_,NodeRef},_}, _Shard, Counters) ->
+    %% TODO: add error info to Counters rather than deleting
+    case fabric_util:remove_down_workers(Counters, NodeRef) of
+        {ok, NewCounters} ->
+            {ok, NewCounters};
+        error ->
+            {error, {nodedown, <<"progress not possible">>}}
+    end;
+
+handle_set_message({rexi_EXIT, Reason}, Shard, Counters) ->
+    %% TODO: add error info to Counters rather than deleting
+    NewCounters = fabric_dict:erase(Shard, Counters),
+    case fabric_view:is_progress_possible(NewCounters) of
+        true ->
+            {ok, NewCounters};
+        false ->
+            {error, Reason}
+    end;
+
+handle_set_message(Else, _Shard, _Acc) ->
+    %% TODO: do we want this behavior change?
+    {error, Else}.
+
 
 merge_results(Info) ->
     Dict = lists:foldl(fun({K,V},D0) -> orddict:append(K,V,D0) end,
@@ -111,6 +151,29 @@
             Acc
     end, [{instance_start_time, <<"0">>}], Dict).
 
+
+format_results(Counters) ->
+    dict:to_list(lists:foldl(
+        fun({S,I0}, D) ->
+            #shard{
+               range = [B, E],
+               node = Node
+            } = S,
+            I = [{node, Node} | I0],
+            HB = couch_util:to_hex(<<B:32/integer>>),
+            HE = couch_util:to_hex(<<E:32/integer>>),
+            R = list_to_binary(HB ++ "-" ++ HE),
+            case dict:find(R, D) of
+                {ok, L} ->
+                    dict:store(R, [{I} | L], D);
+                error ->
+                    dict:store(R, [{I}], D)
+            end
+        end,
+        dict:new(),
+        fabric_dict:to_list(Counters)
+    )).
+
 merge_other_results(Results) ->
     Dict = lists:foldl(fun({Props}, D) ->
         lists:foldl(fun({K,V},D0) -> orddict:append(K,V,D0) end, D, Props)