blob: f94cc28be26a5fde3d3e61caab1a9c0349b9dc1a [file] [log] [blame]
% 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.
-module(couch_query_servers).
-behaviour(gen_server).
-export([start_link/0]).
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]).
-export([start_doc_map/2, map_docs/2, stop_doc_map/1]).
-export([reduce/3, rereduce/3,validate_doc_update/5]).
-export([render_doc_show/6, render_doc_update/6, start_view_list/2,
render_list_head/4, render_list_row/4, render_list_tail/1]).
-export([start_filter/2, filter_doc/4, end_filter/1]).
% -export([test/0]).
-include("couch_db.hrl").
-record(proc, {
pid,
lang,
prompt_fun,
set_timeout_fun,
stop_fun
}).
start_link() ->
gen_server:start_link({local, couch_query_servers}, couch_query_servers, [], []).
stop() ->
exit(whereis(couch_query_servers), close).
start_doc_map(Lang, Functions) ->
Proc = get_os_process(Lang),
lists:foreach(fun(FunctionSource) ->
true = proc_prompt(Proc, [<<"add_fun">>, FunctionSource])
end, Functions),
{ok, Proc}.
map_docs(Proc, Docs) ->
% send the documents
Results = lists:map(
fun(Doc) ->
Json = couch_doc:to_json_obj(Doc, []),
FunsResults = proc_prompt(Proc, [<<"map_doc">>, Json]),
% the results are a json array of function map yields like this:
% [FunResults1, FunResults2 ...]
% where funresults is are json arrays of key value pairs:
% [[Key1, Value1], [Key2, Value2]]
% Convert the key, value pairs to tuples like
% [{Key1, Value1}, {Key2, Value2}]
lists:map(
fun(FunRs) ->
[list_to_tuple(FunResult) || FunResult <- FunRs]
end,
FunsResults)
end,
Docs),
{ok, Results}.
stop_doc_map(nil) ->
ok;
stop_doc_map(Proc) ->
ok = ret_os_process(Proc).
group_reductions_results([]) ->
[];
group_reductions_results(List) ->
{Heads, Tails} = lists:foldl(
fun([H|T], {HAcc,TAcc}) ->
{[H|HAcc], [T|TAcc]}
end, {[], []}, List),
case Tails of
[[]|_] -> % no tails left
[Heads];
_ ->
[Heads | group_reductions_results(Tails)]
end.
rereduce(_Lang, [], _ReducedValues) ->
{ok, []};
rereduce(Lang, RedSrcs, ReducedValues) ->
Proc = get_os_process(Lang),
Grouped = group_reductions_results(ReducedValues),
Results = try lists:zipwith(
fun
(<<"_", _/binary>> = FunSrc, Values) ->
{ok, [Result]} = builtin_reduce(rereduce, [FunSrc], [[[], V] || V <- Values], []),
Result;
(FunSrc, Values) ->
[true, [Result]] =
proc_prompt(Proc, [<<"rereduce">>, [FunSrc], Values]),
Result
end, RedSrcs, Grouped)
after
ok = ret_os_process(Proc)
end,
{ok, Results}.
reduce(_Lang, [], _KVs) ->
{ok, []};
reduce(Lang, RedSrcs, KVs) ->
{OsRedSrcs, BuiltinReds} = lists:partition(fun
(<<"_", _/binary>>) -> false;
(_OsFun) -> true
end, RedSrcs),
{ok, OsResults} = os_reduce(Lang, OsRedSrcs, KVs),
{ok, BuiltinResults} = builtin_reduce(reduce, BuiltinReds, KVs, []),
recombine_reduce_results(RedSrcs, OsResults, BuiltinResults, []).
recombine_reduce_results([], [], [], Acc) ->
{ok, lists:reverse(Acc)};
recombine_reduce_results([<<"_", _/binary>>|RedSrcs], OsResults, [BRes|BuiltinResults], Acc) ->
recombine_reduce_results(RedSrcs, OsResults, BuiltinResults, [BRes|Acc]);
recombine_reduce_results([_OsFun|RedSrcs], [OsR|OsResults], BuiltinResults, Acc) ->
recombine_reduce_results(RedSrcs, OsResults, BuiltinResults, [OsR|Acc]).
os_reduce(_Lang, [], _KVs) ->
{ok, []};
os_reduce(Lang, OsRedSrcs, KVs) ->
Proc = get_os_process(Lang),
OsResults = try proc_prompt(Proc, [<<"reduce">>, OsRedSrcs, KVs]) of
[true, Reductions] -> Reductions
after
ok = ret_os_process(Proc)
end,
{ok, OsResults}.
builtin_reduce(_Re, [], _KVs, Acc) ->
{ok, lists:reverse(Acc)};
builtin_reduce(Re, [<<"_sum">>|BuiltinReds], KVs, Acc) ->
Sum = builtin_sum_rows(KVs),
builtin_reduce(Re, BuiltinReds, KVs, [Sum|Acc]);
builtin_reduce(reduce, [<<"_count">>|BuiltinReds], KVs, Acc) ->
Count = length(KVs),
builtin_reduce(reduce, BuiltinReds, KVs, [Count|Acc]);
builtin_reduce(rereduce, [<<"_count">>|BuiltinReds], KVs, Acc) ->
Count = builtin_sum_rows(KVs),
builtin_reduce(rereduce, BuiltinReds, KVs, [Count|Acc]).
builtin_sum_rows(KVs) ->
lists:foldl(fun
([_Key, Value], Acc) when is_number(Value) ->
Acc + Value;
(_Else, _Acc) ->
throw({invalid_value, <<"builtin _sum function requires map values to be numbers">>})
end, 0, KVs).
validate_doc_update(Lang, FunSrc, EditDoc, DiskDoc, Ctx) ->
Proc = get_os_process(Lang),
JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
JsonDiskDoc =
if DiskDoc == nil ->
null;
true ->
couch_doc:to_json_obj(DiskDoc, [revs])
end,
try proc_prompt(Proc,
[<<"validate">>, FunSrc, JsonEditDoc, JsonDiskDoc, Ctx]) of
1 ->
ok;
{[{<<"forbidden">>, Message}]} ->
throw({forbidden, Message});
{[{<<"unauthorized">>, Message}]} ->
throw({unauthorized, Message})
after
ok = ret_os_process(Proc)
end.
% todo use json_apply_field
append_docid(DocId, JsonReqIn) ->
[{<<"docId">>, DocId} | JsonReqIn].
render_doc_show(Lang, ShowSrc, DocId, Doc, Req, Db) ->
Proc = get_os_process(Lang),
{JsonReqIn} = couch_httpd_external:json_req_obj(Req, Db),
{JsonReq, JsonDoc} = case {DocId, Doc} of
{nil, nil} -> {{JsonReqIn}, null};
{DocId, nil} -> {{append_docid(DocId, JsonReqIn)}, null};
_ -> {{append_docid(DocId, JsonReqIn)}, couch_doc:to_json_obj(Doc, [revs])}
end,
try proc_prompt(Proc,
[<<"show">>, ShowSrc, JsonDoc, JsonReq]) of
FormResp ->
FormResp
after
ok = ret_os_process(Proc)
end.
render_doc_update(Lang, UpdateSrc, DocId, Doc, Req, Db) ->
Proc = get_os_process(Lang),
{JsonReqIn} = couch_httpd_external:json_req_obj(Req, Db),
{JsonReq, JsonDoc} = case {DocId, Doc} of
{nil, nil} -> {{JsonReqIn}, null};
{DocId, nil} -> {{append_docid(DocId, JsonReqIn)}, null};
_ -> {{append_docid(DocId, JsonReqIn)}, couch_doc:to_json_obj(Doc, [revs])}
end,
try proc_prompt(Proc,
[<<"update">>, UpdateSrc, JsonDoc, JsonReq]) of
FormResp ->
FormResp
after
ok = ret_os_process(Proc)
end.
start_view_list(Lang, ListSrc) ->
Proc = get_os_process(Lang),
proc_prompt(Proc, [<<"add_fun">>, ListSrc]),
{ok, Proc}.
render_list_head(Proc, Req, Db, Head) ->
JsonReq = couch_httpd_external:json_req_obj(Req, Db),
proc_prompt(Proc, [<<"list">>, Head, JsonReq]).
render_list_row(Proc, Db, {{Key, DocId}, Value}, IncludeDoc) ->
JsonRow = couch_httpd_view:view_row_obj(Db, {{Key, DocId}, Value}, IncludeDoc),
proc_prompt(Proc, [<<"list_row">>, JsonRow]);
render_list_row(Proc, _, {Key, Value}, _IncludeDoc) ->
JsonRow = {[{key, Key}, {value, Value}]},
proc_prompt(Proc, [<<"list_row">>, JsonRow]).
render_list_tail(Proc) ->
JsonResp = proc_prompt(Proc, [<<"list_end">>]),
ok = ret_os_process(Proc),
JsonResp.
start_filter(Lang, FilterSrc) ->
Proc = get_os_process(Lang),
true = proc_prompt(Proc, [<<"add_fun">>, FilterSrc]),
{ok, Proc}.
filter_doc(Proc, Doc, Req, Db) ->
JsonReq = couch_httpd_external:json_req_obj(Req, Db),
JsonDoc = couch_doc:to_json_obj(Doc, [revs]),
JsonCtx = couch_util:json_user_ctx(Db),
[true, [Pass]] = proc_prompt(Proc,
[<<"filter">>, [JsonDoc], JsonReq, JsonCtx]),
{ok, Pass}.
end_filter(Proc) ->
ok = ret_os_process(Proc).
init([]) ->
% read config and register for configuration changes
% just stop if one of the config settings change. couch_server_sup
% will restart us and then we will pick up the new settings.
ok = couch_config:register(
fun("query_servers" ++ _, _) ->
?MODULE:stop()
end),
ok = couch_config:register(
fun("native_query_servers" ++ _, _) ->
?MODULE:stop()
end),
Langs = ets:new(couch_query_server_langs, [set, private]),
PidProcs = ets:new(couch_query_server_pid_langs, [set, private]),
LangProcs = ets:new(couch_query_server_procs, [set, private]),
InUse = ets:new(couch_query_server_used, [set, private]),
% 'query_servers' specifies an OS command-line to execute.
lists:foreach(fun({Lang, Command}) ->
true = ets:insert(Langs, {?l2b(Lang),
couch_os_process, start_link, [Command]})
end, couch_config:get("query_servers")),
% 'native_query_servers' specifies a {Module, Func, Arg} tuple.
lists:foreach(fun({Lang, SpecStr}) ->
{ok, {Mod, Fun, SpecArg}} = couch_util:parse_term(SpecStr),
true = ets:insert(Langs, {?l2b(Lang),
Mod, Fun, SpecArg})
end, couch_config:get("native_query_servers")),
process_flag(trap_exit, true),
{ok, {Langs, % Keyed by language name, value is {Mod,Func,Arg}
PidProcs, % Keyed by PID, valus is a #proc record.
LangProcs, % Keyed by language name, value is a #proc record
InUse % Keyed by PID, value is #proc record.
}}.
terminate(_Reason, _Server) ->
ok.
handle_call({get_proc, Lang}, _From, {Langs, PidProcs, LangProcs, InUse}=Server) ->
% Note to future self. Add max process limit.
case ets:lookup(LangProcs, Lang) of
[{Lang, [Proc|_]}] ->
add_value(PidProcs, Proc#proc.pid, Proc),
rem_from_list(LangProcs, Lang, Proc),
add_to_list(InUse, Lang, Proc),
{reply, {recycled, Proc, get_query_server_config()}, Server};
_ ->
case (catch new_process(Langs, Lang)) of
{ok, Proc} ->
add_to_list(InUse, Lang, Proc),
{reply, {new, Proc}, Server};
Error ->
{reply, Error, Server}
end
end;
handle_call({ret_proc, Proc}, _From, {_, _, LangProcs, InUse}=Server) ->
% Along with max process limit, here we should check
% if we're over the limit and discard when we are.
add_to_list(LangProcs, Proc#proc.lang, Proc),
rem_from_list(InUse, Proc#proc.lang, Proc),
{reply, true, Server}.
handle_cast(_Whatever, Server) ->
{noreply, Server}.
handle_info({'EXIT', Pid, Status}, {_, PidProcs, LangProcs, InUse}=Server) ->
case ets:lookup(PidProcs, Pid) of
[{Pid, Proc}] ->
case Status of
normal -> ok;
_ -> ?LOG_DEBUG("Linked process died abnormally: ~p (reason: ~p)", [Pid, Status])
end,
rem_value(PidProcs, Pid),
catch rem_from_list(LangProcs, Proc#proc.lang, Proc),
catch rem_from_list(InUse, Proc#proc.lang, Proc),
{noreply, Server};
[] ->
?LOG_DEBUG("Unknown linked process died: ~p (reason: ~p)", [Pid, Status]),
{stop, Status, Server}
end.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
% Private API
get_query_server_config() ->
ReduceLimit = list_to_atom(
couch_config:get("query_server_config","reduce_limit","true")),
{[{<<"reduce_limit">>, ReduceLimit}]}.
new_process(Langs, Lang) ->
case ets:lookup(Langs, Lang) of
[{Lang, Mod, Func, Arg}] ->
{ok, Pid} = apply(Mod, Func, Arg),
{ok, #proc{lang=Lang,
pid=Pid,
% Called via proc_prompt, proc_set_timeout, and proc_stop
prompt_fun={Mod, prompt},
set_timeout_fun={Mod, set_timeout},
stop_fun={Mod, stop}}};
_ ->
{unknown_query_language, Lang}
end.
proc_prompt(Proc, Args) ->
{Mod, Func} = Proc#proc.prompt_fun,
apply(Mod, Func, [Proc#proc.pid, Args]).
proc_stop(Proc) ->
{Mod, Func} = Proc#proc.stop_fun,
apply(Mod, Func, [Proc#proc.pid]).
proc_set_timeout(Proc, Timeout) ->
{Mod, Func} = Proc#proc.set_timeout_fun,
apply(Mod, Func, [Proc#proc.pid, Timeout]).
get_os_process(Lang) ->
case gen_server:call(couch_query_servers, {get_proc, Lang}) of
{new, Proc} ->
proc_set_timeout(Proc, list_to_integer(couch_config:get(
"couchdb", "os_process_timeout", "5000"))),
link(Proc#proc.pid),
Proc;
{recycled, Proc, QueryConfig} ->
case (catch proc_prompt(Proc, [<<"reset">>, QueryConfig])) of
true ->
proc_set_timeout(Proc, list_to_integer(couch_config:get(
"couchdb", "os_process_timeout", "5000"))),
link(Proc#proc.pid),
Proc;
_ ->
catch proc_stop(Proc),
get_os_process(Lang)
end;
Error ->
throw(Error)
end.
ret_os_process(Proc) ->
true = gen_server:call(couch_query_servers, {ret_proc, Proc}),
catch unlink(Proc#proc.pid),
ok.
add_value(Tid, Key, Value) ->
true = ets:insert(Tid, {Key, Value}).
rem_value(Tid, Key) ->
true = ets:delete(Tid, Key).
add_to_list(Tid, Key, Value) ->
case ets:lookup(Tid, Key) of
[{Key, Vals}] ->
true = ets:insert(Tid, {Key, [Value|Vals]});
[] ->
true = ets:insert(Tid, {Key, [Value]})
end.
rem_from_list(Tid, Key, Value) ->
case ets:lookup(Tid, Key) of
[{Key, Vals}] ->
ets:insert(Tid, {Key, [Val || Val <- Vals, Val /= Value]});
[] -> ok
end.