diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index dd63db0..ad4e9f9 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -33,6 +33,7 @@ javascript = %bindir%/%couchjs_command_name% %localdatadir%/server/main.js view_manager={couch_view, start_link, []} external_manager={couch_external_manager, start_link, []} db_update_notifier={couch_db_update_notifier_sup, start_link, []} +doc_update_notifier={couch_doc_update_notifier_sup, start_link, []} query_servers={couch_query_servers, start_link, []} httpd={couch_httpd, start_link, []} stats_aggregator={couch_stats_aggregator, start, []} diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am index 8bea529..effc168 100644 --- a/src/couchdb/Makefile.am +++ b/src/couchdb/Makefile.am @@ -46,6 +46,8 @@ source_files = \ couch_db.erl \ couch_db_update_notifier.erl \ couch_db_update_notifier_sup.erl \ + couch_doc_update_notifier.erl \ + couch_doc_update_notifier_sup.erl \ couch_doc.erl \ couch_event_sup.erl \ couch_external_manager.erl \ @@ -87,6 +89,8 @@ compiled_files = \ couch_db.beam \ couch_db_update_notifier.beam \ couch_db_update_notifier_sup.beam \ + couch_doc_update_notifier.beam \ + couch_doc_update_notifier_sup.beam \ couch_doc.beam \ couch_event_sup.beam \ couch_external_manager.beam \ @@ -133,6 +137,8 @@ compiled_files = \ # couch_db.html \ # couch_db_update_notifier.html \ # couch_db_update_notifier_sup.html \ +# couch_doc_update_notifier.html \ +# couch_doc_update_notifier_sup.html \ # couch_doc.html \ # couch_event_sup.html \ # couch_file.html \ diff --git a/src/couchdb/Makefile.in b/src/couchdb/Makefile.in index 404f48e..e274cbe 100644 --- a/src/couchdb/Makefile.in +++ b/src/couchdb/Makefile.in @@ -271,6 +271,8 @@ source_files = \ couch_db.erl \ couch_db_update_notifier.erl \ couch_db_update_notifier_sup.erl \ + couch_doc_update_notifier.erl \ + couch_doc_update_notifier_sup.erl \ couch_doc.erl \ couch_event_sup.erl \ couch_external_manager.erl \ @@ -311,6 +313,8 @@ compiled_files = \ couch_db.beam \ couch_db_update_notifier.beam \ couch_db_update_notifier_sup.beam \ + couch_doc_update_notifier.beam \ + couch_doc_update_notifier_sup.beam \ couch_doc.beam \ couch_event_sup.beam \ couch_external_manager.beam \ @@ -750,6 +754,8 @@ uninstall-am: uninstall-couchebinDATA uninstall-couchincludeDATA \ # couch_db.html \ # couch_db_update_notifier.html \ # couch_db_update_notifier_sup.html \ +# couch_doc_update_notifier.html \ +# couch_doc_update_notifier_sup.html \ # couch_doc.html \ # couch_event_sup.html \ # couch_file.html \ diff --git a/src/couchdb/couch.app.tpl.in b/src/couchdb/couch.app.tpl.in index e0100cb..bbca2e8 100644 --- a/src/couchdb/couch.app.tpl.in +++ b/src/couchdb/couch.app.tpl.in @@ -17,11 +17,14 @@ couch_event_sup, couch_db_update_notifier, couch_db_update_notifier_sup, + couch_doc_update_notifier, + couch_doc_update_notifier_sup, couch_log, couch_rep]}, {registered,[couch_server, couch_server_sup, couch_view, couch_query_servers, - couch_db_update_notifier_sup]}, + couch_db_update_notifier_sup, + couch_doc_update_notifier_sup]}, {applications,[kernel,stdlib,crypto,ibrowse,mochiweb]}]}. diff --git a/src/couchdb/couch_doc_update_notifier.erl b/src/couchdb/couch_doc_update_notifier.erl new file mode 100644 index 0000000..95252ca --- /dev/null +++ b/src/couchdb/couch_doc_update_notifier.erl @@ -0,0 +1,74 @@ +% 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. + +% +% This causes an OS process to spawned and it is notified every time a database +% is updated. +% +% The notifications are in the form of a the database name sent as a line of +% text to the OS processes stdout. +% + +-module(couch_doc_update_notifier). + +-behaviour(gen_event). + +-export([start_link/1, notify/1]). +-export([init/1, terminate/2, handle_event/2, handle_call/2, handle_info/2, code_change/3,stop/1]). + +-include("couch_db.hrl"). + +start_link(Exec) -> + couch_event_sup:start_link(couch_doc_update, {couch_doc_update_notifier, make_ref()}, Exec). + +notify(Event) -> + gen_event:notify(couch_doc_update, Event). + +stop(Pid) -> + couch_event_sup:stop(Pid). + +init(Exec) when is_list(Exec) -> % an exe + couch_os_process:start_link(Exec, [], [stream, exit_status, hide]); +init(Else) -> + {ok, Else}. + +terminate(_Reason, Pid) when is_pid(Pid) -> + couch_os_process:stop(Pid), + ok; +terminate(_Reason, _State) -> + ok. + +handle_event(Event, Fun) when is_function(Fun, 1) -> + Fun(Event), + {ok, Fun}; +handle_event(Event, {Fun, FunAcc}) -> + FunAcc2 = Fun(Event, FunAcc), + {ok, {Fun, FunAcc2}}; +handle_event({EventAtom, DbName, DocId, OldDoc, NewDoc}, Pid) -> + Obj = {[{type, list_to_binary(atom_to_list(EventAtom))}, {db, DbName}, + {id, DocId}, {old, OldDoc}, {new, NewDoc}]}, + true = couch_os_process:write(Pid, Obj), + {ok, Pid}. + +handle_call(_Request, State) -> + {reply, ok, State}. + +handle_info({'EXIT', Pid, Reason}, Pid) -> + ?LOG_ERROR("Document Update notification process ~p died: ~p", [Pid, Reason]), + remove_handler; +handle_info({'EXIT', _, _}, Pid) -> + %% the db_update event manager traps exits and forwards this message to all + %% its handlers. Just ignore as it wasn't our os_process that exited. + {ok, Pid}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/couchdb/couch_doc_update_notifier_sup.erl b/src/couchdb/couch_doc_update_notifier_sup.erl new file mode 100644 index 0000000..b67601b --- /dev/null +++ b/src/couchdb/couch_doc_update_notifier_sup.erl @@ -0,0 +1,50 @@ +% 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. + +% +% This causes an OS process to spawned and it is notified every time a document +% is updated. +% +% The notifications are in the form of a record conisting of the database name, +% the doc id, the old doc and the new doc, sent as a line of text to the OS +% process' stdout. +% + +-module(couch_doc_update_notifier_sup). + +-behaviour(supervisor). + +-export([start_link/0,init/1]). + +start_link() -> + supervisor:start_link({local, couch_doc_update_notifier_sup}, + couch_doc_update_notifier_sup, []). + +init([]) -> + Self = self(), + ok = couch_config:register( + fun("doc_update_notification", _) -> + exit(Self, reload_config) + end), + + UpdateNotifierExes = couch_config:get("doc_update_notification"), + + {ok, + {{one_for_one, 10, 3600}, + lists:map(fun({Name, UpdateNotifierExe}) -> + {Name, + {couch_doc_update_notifier, start_link, [UpdateNotifierExe]}, + permanent, + 1000, + supervisor, + [couch_doc_update_notifier]} + end, UpdateNotifierExes)}}. diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index 2bfc294..2c3c819 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -90,6 +90,8 @@ db_req(#httpd{method='POST',path_parts=[DbName]}=Req, Db) -> Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)), DocId = couch_util:new_uuid(), {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId}, []), + OldDoc = #doc{}, + couch_doc_update_notifier:notify({create, DbName, DocId, OldDoc#doc.body, Doc#doc.body}), DocUrl = absolute_uri(Req, binary_to_list(<<"/",DbName/binary,"/",DocId/binary>>)), send_json(Req, 201, [{"Location", DocUrl}], {[ @@ -422,15 +424,18 @@ all_docs_view(Req, Db, Keys) -> -db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> +db_doc_req(#httpd{method='DELETE',path_parts=[DbName|_]}=Req, Db, DocId) -> % check for the existence of the doc to handle the 404 case. - couch_doc_open(Db, DocId, nil, []), - case couch_httpd:qs_value(Req, "rev") of - undefined -> - update_doc(Req, Db, DocId, {[{<<"_deleted">>,true}]}); - Rev -> - update_doc(Req, Db, DocId, {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}) - end; + #doc{body=OldBody} = couch_doc_open(Db, DocId, nil, []), + ReturnValue = case couch_httpd:qs_value(Req, "rev") of + undefined -> + update_doc(Req, Db, DocId, {[{<<"_deleted">>,true}]}); + Rev -> + update_doc(Req, Db, DocId, {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}) + end, + NewDoc = #doc{}, + couch_doc_update_notifier:notify({delete, DbName, DocId, OldBody, NewDoc#doc.body}), + ReturnValue; db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> #doc_query_args{ @@ -495,8 +500,21 @@ db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> {rev, couch_doc:rev_to_str(NewRev)} ]}); -db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) -> - update_doc(Req, Db, DocId, couch_httpd:json_body(Req)); +db_doc_req(#httpd{method='PUT',path_parts=[DbName|_]}=Req, Db, DocId) -> + % get old doc + case couch_db:open_doc(Db, DocId, []) of + {ok, DocData} -> + #doc{body=OldBody} = DocData, + Operation = update; + Error -> + #doc{body=OldBody} = #doc{}, + Operation = create + end, + NewDoc = couch_httpd:json_body(Req), + ReturnValue = update_doc(Req, Db, DocId, NewDoc), + #doc{body=NewBody} = couch_doc:from_json_obj(NewDoc), + couch_doc_update_notifier:notify({Operation, DbName, DocId, OldBody, NewBody}), + ReturnValue; db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) -> SourceRev = diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl index 15867ad..a899066 100644 --- a/src/couchdb/couch_server_sup.erl +++ b/src/couchdb/couch_server_sup.erl @@ -156,6 +156,12 @@ start_primary_services() -> permanent, brutal_kill, supervisor, + dynamic}, + {couch_doc_update_event, + {gen_event, start_link, [{local, couch_doc_update}]}, + permanent, + brutal_kill, + supervisor, dynamic} ] }).