PageRenderTime 59ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/apps/couch/src/couch_rep.erl

http://github.com/cloudant/bigcouch
Erlang | 932 lines | 800 code | 88 blank | 44 comment | 19 complexity | 5f4428816af4747473ead855c321aa50 MD5 | raw file
Possible License(s): Apache-2.0
  1. % Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2. % use this file except in compliance with the License. You may obtain a copy of
  3. % the License at
  4. %
  5. % http://www.apache.org/licenses/LICENSE-2.0
  6. %
  7. % Unless required by applicable law or agreed to in writing, software
  8. % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. % License for the specific language governing permissions and limitations under
  11. % the License.
  12. -module(couch_rep).
  13. -behaviour(gen_server).
  14. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
  15. code_change/3]).
  16. -export([replicate/2, replicate/3, checkpoint/1]).
  17. -export([make_replication_id/2]).
  18. -include("couch_db.hrl").
  19. -include("couch_js_functions.hrl").
  20. -include_lib("ibrowse/include/ibrowse.hrl").
  21. -define(REP_ID_VERSION, 2).
  22. -record(state, {
  23. changes_feed,
  24. missing_revs,
  25. reader,
  26. writer,
  27. source,
  28. target,
  29. continuous,
  30. create_target,
  31. init_args,
  32. checkpoint_scheduled = nil,
  33. start_seq,
  34. history,
  35. session_id,
  36. source_log,
  37. target_log,
  38. rep_starttime,
  39. src_starttime,
  40. tgt_starttime,
  41. checkpoint_history = nil,
  42. listeners = [],
  43. complete = false,
  44. committed_seq = 0,
  45. stats = nil,
  46. source_db_update_notifier = nil,
  47. target_db_update_notifier = nil
  48. }).
  49. replicate(A, B, _C) ->
  50. replicate(A, B).
  51. %% convenience function to do a simple replication from the shell
  52. replicate(Source, Target) when is_list(Source) ->
  53. replicate(?l2b(Source), Target);
  54. replicate(Source, Target) when is_binary(Source), is_list(Target) ->
  55. replicate(Source, ?l2b(Target));
  56. replicate(Source, Target) when is_binary(Source), is_binary(Target) ->
  57. replicate({[{<<"source">>, Source}, {<<"target">>, Target}]}, #user_ctx{});
  58. %% function handling POST to _replicate
  59. replicate({Props}=PostBody, UserCtx) ->
  60. RepId = make_replication_id(PostBody, UserCtx),
  61. case couch_util:get_value(<<"cancel">>, Props, false) of
  62. true ->
  63. end_replication(RepId);
  64. false ->
  65. Server = start_replication(PostBody, RepId, UserCtx),
  66. get_result(Server, RepId, PostBody, UserCtx)
  67. end.
  68. end_replication({BaseId, Extension}) ->
  69. RepId = BaseId ++ Extension,
  70. case supervisor:terminate_child(couch_rep_sup, RepId) of
  71. {error, not_found} = R ->
  72. R;
  73. ok ->
  74. case supervisor:delete_child(couch_rep_sup, RepId) of
  75. ok ->
  76. {ok, {cancelled, ?l2b(BaseId)}};
  77. {error, not_found} ->
  78. {ok, {cancelled, ?l2b(BaseId)}};
  79. {error, _} = Error ->
  80. Error
  81. end
  82. end.
  83. start_replication(RepDoc, {BaseId, Extension} = RepId, UserCtx) ->
  84. Replicator = {
  85. BaseId ++ Extension,
  86. {gen_server, start_link,
  87. [?MODULE, [RepId, RepDoc, UserCtx], []]},
  88. temporary,
  89. 1,
  90. worker,
  91. [?MODULE]
  92. },
  93. start_replication_server(Replicator).
  94. checkpoint(Server) ->
  95. gen_server:cast(Server, do_checkpoint).
  96. get_result(Server, {BaseId, _Extension}, {Props} = PostBody, UserCtx) ->
  97. case couch_util:get_value(<<"continuous">>, Props, false) of
  98. true ->
  99. {ok, {continuous, ?l2b(BaseId)}};
  100. false ->
  101. try gen_server:call(Server, get_result, infinity) of
  102. retry -> replicate(PostBody, UserCtx);
  103. Else -> Else
  104. catch
  105. exit:{noproc, {gen_server, call, [Server, get_result, infinity]}} ->
  106. %% oops, this replication just finished -- restart it.
  107. replicate(PostBody, UserCtx);
  108. exit:{normal, {gen_server, call, [Server, get_result, infinity]}} ->
  109. %% we made the call during terminate
  110. replicate(PostBody, UserCtx)
  111. end
  112. end.
  113. init(InitArgs) ->
  114. try
  115. do_init(InitArgs)
  116. catch
  117. throw:Error ->
  118. {stop, Error}
  119. end.
  120. do_init([{BaseId, _Ext} = RepId, {PostProps}, UserCtx] = InitArgs) ->
  121. process_flag(trap_exit, true),
  122. SourceProps = couch_util:get_value(<<"source">>, PostProps),
  123. TargetProps = couch_util:get_value(<<"target">>, PostProps),
  124. Continuous = couch_util:get_value(<<"continuous">>, PostProps, false),
  125. CreateTarget = couch_util:get_value(<<"create_target">>, PostProps, false),
  126. ProxyParams = parse_proxy_params(
  127. couch_util:get_value(<<"proxy">>, PostProps, [])),
  128. Source = open_db(SourceProps, UserCtx, ProxyParams),
  129. Target = open_db(TargetProps, UserCtx, ProxyParams, CreateTarget),
  130. SourceInfo = dbinfo(Source),
  131. TargetInfo = dbinfo(Target),
  132. [SourceLog, TargetLog] = find_replication_logs(
  133. [Source, Target], BaseId, {PostProps}, UserCtx),
  134. {StartSeq, History} = compare_replication_logs(SourceLog, TargetLog),
  135. {ok, ChangesFeed} =
  136. couch_rep_changes_feed:start_link(self(), Source, StartSeq, PostProps),
  137. {ok, MissingRevs} =
  138. couch_rep_missing_revs:start_link(self(), Target, ChangesFeed, PostProps),
  139. {ok, Reader} =
  140. couch_rep_reader:start_link(self(), Source, MissingRevs, PostProps),
  141. {ok, Writer} =
  142. couch_rep_writer:start_link(self(), Target, Reader, PostProps),
  143. Stats = ets:new(replication_stats, [set, private]),
  144. ets:insert(Stats, {total_revs,0}),
  145. ets:insert(Stats, {missing_revs, 0}),
  146. ets:insert(Stats, {docs_read, 0}),
  147. ets:insert(Stats, {docs_written, 0}),
  148. ets:insert(Stats, {doc_write_failures, 0}),
  149. couch_task_status:add_task([
  150. {user, UserCtx#user_ctx.name},
  151. {type, replication},
  152. {replication_id, ?l2b(BaseId)},
  153. {source, dbname(Source)},
  154. {target, dbname(Target)},
  155. {continuous, Continuous},
  156. {docs_read, 0},
  157. {docs_written, 0},
  158. {doc_write_failures, 0}
  159. ]),
  160. couch_task_status:set_update_frequency(1000),
  161. State = #state{
  162. changes_feed = ChangesFeed,
  163. missing_revs = MissingRevs,
  164. reader = Reader,
  165. writer = Writer,
  166. source = Source,
  167. target = Target,
  168. continuous = Continuous,
  169. create_target = CreateTarget,
  170. init_args = InitArgs,
  171. stats = Stats,
  172. checkpoint_scheduled = nil,
  173. start_seq = StartSeq,
  174. history = History,
  175. session_id = couch_uuids:random(),
  176. source_log = SourceLog,
  177. target_log = TargetLog,
  178. rep_starttime = httpd_util:rfc1123_date(),
  179. src_starttime = couch_util:get_value(instance_start_time, SourceInfo),
  180. tgt_starttime = couch_util:get_value(instance_start_time, TargetInfo),
  181. source_db_update_notifier = source_db_update_notifier(Source),
  182. target_db_update_notifier = target_db_update_notifier(Target)
  183. },
  184. {ok, State}.
  185. handle_call(get_result, From, #state{complete=true, listeners=[]} = State) ->
  186. {stop, normal, State#state{listeners=[From]}};
  187. handle_call(get_result, From, State) ->
  188. Listeners = State#state.listeners,
  189. {noreply, State#state{listeners=[From|Listeners]}};
  190. handle_call(get_source_db, _From, #state{source = Source} = State) ->
  191. {reply, {ok, Source}, State};
  192. handle_call(get_target_db, _From, #state{target = Target} = State) ->
  193. {reply, {ok, Target}, State}.
  194. handle_cast(reopen_source_db, #state{source = Source} = State) ->
  195. {ok, NewSource} = couch_db:reopen(Source),
  196. {noreply, State#state{source = NewSource}};
  197. handle_cast(reopen_target_db, #state{target = Target} = State) ->
  198. {ok, NewTarget} = couch_db:reopen(Target),
  199. {noreply, State#state{target = NewTarget}};
  200. handle_cast(do_checkpoint, State) ->
  201. {noreply, do_checkpoint(State)};
  202. handle_cast(_Msg, State) ->
  203. {noreply, State}.
  204. handle_info({missing_revs_checkpoint, SourceSeq}, State) ->
  205. NewState = schedule_checkpoint(State#state{committed_seq = SourceSeq}),
  206. update_task(NewState),
  207. {noreply, NewState};
  208. handle_info({writer_checkpoint, SourceSeq}, #state{committed_seq=N} = State)
  209. when SourceSeq > N ->
  210. MissingRevs = State#state.missing_revs,
  211. ok = gen_server:cast(MissingRevs, {update_committed_seq, SourceSeq}),
  212. NewState = schedule_checkpoint(State#state{committed_seq = SourceSeq}),
  213. update_task(NewState),
  214. {noreply, NewState};
  215. handle_info({writer_checkpoint, _}, State) ->
  216. {noreply, State};
  217. handle_info({update_stats, Key, N}, State) ->
  218. ets:update_counter(State#state.stats, Key, N),
  219. {noreply, State};
  220. handle_info({'DOWN', _, _, _, _}, State) ->
  221. ?LOG_INFO("replication terminating because local DB is shutting down", []),
  222. timer:cancel(State#state.checkpoint_scheduled),
  223. {stop, shutdown, State};
  224. handle_info({'EXIT', Writer, normal}, #state{writer=Writer} = State) ->
  225. case State#state.listeners of
  226. [] ->
  227. {noreply, State#state{complete = true}};
  228. _Else ->
  229. {stop, normal, State}
  230. end;
  231. handle_info({'EXIT', _, normal}, State) ->
  232. {noreply, State};
  233. handle_info({'EXIT', _Pid, {Err, Reason}}, State) when Err == source_error;
  234. Err == target_error ->
  235. ?LOG_INFO("replication terminating due to ~p: ~p", [Err, Reason]),
  236. timer:cancel(State#state.checkpoint_scheduled),
  237. {stop, shutdown, State};
  238. handle_info({'EXIT', _Pid, Reason}, State) ->
  239. {stop, Reason, State}.
  240. terminate(normal, #state{checkpoint_scheduled=nil, init_args=[RepId | _]} = State) ->
  241. do_terminate(State);
  242. terminate(normal, #state{init_args=[RepId | _]} = State) ->
  243. timer:cancel(State#state.checkpoint_scheduled),
  244. do_terminate(do_checkpoint(State));
  245. terminate(shutdown, #state{listeners = Listeners} = State) ->
  246. % continuous replication stopped
  247. [gen_server:reply(L, {ok, stopped}) || L <- Listeners],
  248. terminate_cleanup(State);
  249. terminate(Reason, #state{listeners = Listeners, init_args=[RepId | _]} = State) ->
  250. [gen_server:reply(L, {error, Reason}) || L <- Listeners],
  251. terminate_cleanup(State).
  252. code_change(_OldVsn, State, _Extra) ->
  253. {ok, State}.
  254. % internal funs
  255. start_replication_server(Replicator) ->
  256. RepId = element(1, Replicator),
  257. case supervisor:start_child(couch_rep_sup, Replicator) of
  258. {ok, Pid} ->
  259. ?LOG_INFO("starting new replication ~p at ~p", [RepId, Pid]),
  260. Pid;
  261. {error, already_present} ->
  262. case supervisor:restart_child(couch_rep_sup, RepId) of
  263. {ok, Pid} ->
  264. ?LOG_INFO("starting replication ~p at ~p", [RepId, Pid]),
  265. Pid;
  266. {error, running} ->
  267. %% this error occurs if multiple replicators are racing
  268. %% each other to start and somebody else won. Just grab
  269. %% the Pid by calling start_child again.
  270. {error, {already_started, Pid}} =
  271. supervisor:start_child(couch_rep_sup, Replicator),
  272. ?LOG_DEBUG("replication ~p already running at ~p", [RepId, Pid]),
  273. Pid;
  274. {error, {db_not_found, DbUrl}} ->
  275. throw({db_not_found, <<"could not open ", DbUrl/binary>>});
  276. {error, {unauthorized, DbUrl}} ->
  277. throw({unauthorized,
  278. <<"unauthorized to access or create database ", DbUrl/binary>>});
  279. {error, {'EXIT', {badarg,
  280. [{erlang, apply, [gen_server, start_link, undefined]} | _]}}} ->
  281. % Clause to deal with a change in the supervisor module introduced
  282. % in R14B02. For more details consult the thread at:
  283. % http://erlang.org/pipermail/erlang-bugs/2011-March/002273.html
  284. _ = supervisor:delete_child(couch_rep_sup, RepId),
  285. start_replication_server(Replicator)
  286. end;
  287. {error, {already_started, Pid}} ->
  288. ?LOG_DEBUG("replication ~p already running at ~p", [RepId, Pid]),
  289. Pid;
  290. {error, {{db_not_found, DbUrl}, _}} ->
  291. throw({db_not_found, <<"could not open ", DbUrl/binary>>});
  292. {error, {{unauthorized, DbUrl}, _}} ->
  293. throw({unauthorized,
  294. <<"unauthorized to access or create database ", DbUrl/binary>>})
  295. end.
  296. compare_replication_logs(SrcDoc, TgtDoc) ->
  297. #doc{body={RepRecProps}} = SrcDoc,
  298. #doc{body={RepRecPropsTgt}} = TgtDoc,
  299. case couch_util:get_value(<<"session_id">>, RepRecProps) ==
  300. couch_util:get_value(<<"session_id">>, RepRecPropsTgt) of
  301. true ->
  302. % if the records have the same session id,
  303. % then we have a valid replication history
  304. OldSeqNum = couch_util:get_value(<<"source_last_seq">>, RepRecProps, 0),
  305. OldHistory = couch_util:get_value(<<"history">>, RepRecProps, []),
  306. {OldSeqNum, OldHistory};
  307. false ->
  308. SourceHistory = couch_util:get_value(<<"history">>, RepRecProps, []),
  309. TargetHistory = couch_util:get_value(<<"history">>, RepRecPropsTgt, []),
  310. ?LOG_INFO("Replication records differ. "
  311. "Scanning histories to find a common ancestor.", []),
  312. ?LOG_DEBUG("Record on source:~p~nRecord on target:~p~n",
  313. [RepRecProps, RepRecPropsTgt]),
  314. compare_rep_history(SourceHistory, TargetHistory)
  315. end.
  316. compare_rep_history(S, T) when S =:= [] orelse T =:= [] ->
  317. ?LOG_INFO("no common ancestry -- performing full replication", []),
  318. {0, []};
  319. compare_rep_history([{S}|SourceRest], [{T}|TargetRest]=Target) ->
  320. SourceId = couch_util:get_value(<<"session_id">>, S),
  321. case has_session_id(SourceId, Target) of
  322. true ->
  323. RecordSeqNum = couch_util:get_value(<<"recorded_seq">>, S, 0),
  324. ?LOG_INFO("found a common replication record with source_seq ~p",
  325. [RecordSeqNum]),
  326. {RecordSeqNum, SourceRest};
  327. false ->
  328. TargetId = couch_util:get_value(<<"session_id">>, T),
  329. case has_session_id(TargetId, SourceRest) of
  330. true ->
  331. RecordSeqNum = couch_util:get_value(<<"recorded_seq">>, T, 0),
  332. ?LOG_INFO("found a common replication record with source_seq ~p",
  333. [RecordSeqNum]),
  334. {RecordSeqNum, TargetRest};
  335. false ->
  336. compare_rep_history(SourceRest, TargetRest)
  337. end
  338. end.
  339. close_db(#http_db{}) ->
  340. ok;
  341. close_db(Db) ->
  342. couch_db:close(Db).
  343. dbname(#http_db{url = Url}) ->
  344. couch_util:url_strip_password(Url);
  345. dbname(#db{name = Name}) ->
  346. Name.
  347. dbinfo(#http_db{} = Db) ->
  348. {DbProps} = couch_rep_httpc:request(Db),
  349. [{couch_util:to_existing_atom(K), V} || {K,V} <- DbProps];
  350. dbinfo(Db) ->
  351. {ok, Info} = couch_db:get_db_info(Db),
  352. Info.
  353. do_terminate(State) ->
  354. #state{
  355. checkpoint_history = CheckpointHistory,
  356. committed_seq = NewSeq,
  357. listeners = Listeners,
  358. source = Source,
  359. continuous = Continuous,
  360. source_log = #doc{body={OldHistory}}
  361. } = State,
  362. NewRepHistory = case CheckpointHistory of
  363. nil ->
  364. {[{<<"no_changes">>, true} | OldHistory]};
  365. _Else ->
  366. CheckpointHistory
  367. end,
  368. %% reply to original requester
  369. OtherListeners = case Continuous of
  370. true ->
  371. []; % continuous replications have no listeners
  372. _ ->
  373. [Original|Rest] = lists:reverse(Listeners),
  374. gen_server:reply(Original, {ok, NewRepHistory}),
  375. Rest
  376. end,
  377. %% maybe trigger another replication. If this replicator uses a local
  378. %% source Db, changes to that Db since we started will not be included in
  379. %% this pass.
  380. case up_to_date(Source, NewSeq) of
  381. true ->
  382. [gen_server:reply(R, {ok, NewRepHistory}) || R <- OtherListeners];
  383. false ->
  384. [gen_server:reply(R, retry) || R <- OtherListeners]
  385. end,
  386. terminate_cleanup(State).
  387. terminate_cleanup(State) ->
  388. close_db(State#state.source),
  389. close_db(State#state.target),
  390. stop_db_update_notifier(State#state.source_db_update_notifier),
  391. stop_db_update_notifier(State#state.target_db_update_notifier),
  392. ets:delete(State#state.stats).
  393. stop_db_update_notifier(nil) ->
  394. ok;
  395. stop_db_update_notifier(Notifier) ->
  396. couch_db_update_notifier:stop(Notifier).
  397. has_session_id(_SessionId, []) ->
  398. false;
  399. has_session_id(SessionId, [{Props} | Rest]) ->
  400. case couch_util:get_value(<<"session_id">>, Props, nil) of
  401. SessionId ->
  402. true;
  403. _Else ->
  404. has_session_id(SessionId, Rest)
  405. end.
  406. maybe_append_options(Options, {Props}) ->
  407. lists:foldl(fun(Option, Acc) ->
  408. Acc ++
  409. case couch_util:get_value(Option, Props, false) of
  410. true ->
  411. "+" ++ ?b2l(Option);
  412. false ->
  413. ""
  414. end
  415. end, [], Options).
  416. make_replication_id(RepProps, UserCtx) ->
  417. BaseId = make_replication_id(RepProps, UserCtx, ?REP_ID_VERSION),
  418. Extension = maybe_append_options(
  419. [<<"continuous">>, <<"create_target">>], RepProps),
  420. {BaseId, Extension}.
  421. % Versioned clauses for generating replication ids
  422. % If a change is made to how replications are identified
  423. % add a new clause and increase ?REP_ID_VERSION at the top
  424. make_replication_id({Props}, UserCtx, 2) ->
  425. {ok, HostName} = inet:gethostname(),
  426. Port = case (catch mochiweb_socket_server:get(couch_httpd, port)) of
  427. P when is_number(P) ->
  428. P;
  429. _ ->
  430. % On restart we might be called before the couch_httpd process is
  431. % started.
  432. % TODO: we might be under an SSL socket server only, or both under
  433. % SSL and a non-SSL socket.
  434. % ... mochiweb_socket_server:get(https, port)
  435. list_to_integer(couch_config:get("httpd", "port", "5984"))
  436. end,
  437. Src = get_rep_endpoint(UserCtx, couch_util:get_value(<<"source">>, Props)),
  438. Tgt = get_rep_endpoint(UserCtx, couch_util:get_value(<<"target">>, Props)),
  439. maybe_append_filters({Props}, [HostName, Port, Src, Tgt], UserCtx);
  440. make_replication_id({Props}, UserCtx, 1) ->
  441. {ok, HostName} = inet:gethostname(),
  442. Src = get_rep_endpoint(UserCtx, couch_util:get_value(<<"source">>, Props)),
  443. Tgt = get_rep_endpoint(UserCtx, couch_util:get_value(<<"target">>, Props)),
  444. maybe_append_filters({Props}, [HostName, Src, Tgt], UserCtx).
  445. maybe_append_filters({Props}, Base, UserCtx) ->
  446. Base2 = Base ++
  447. case couch_util:get_value(<<"filter">>, Props) of
  448. undefined ->
  449. case couch_util:get_value(<<"doc_ids">>, Props) of
  450. undefined ->
  451. [];
  452. DocIds ->
  453. [DocIds]
  454. end;
  455. Filter ->
  456. [filter_code(Filter, Props, UserCtx),
  457. couch_util:get_value(<<"query_params">>, Props, {[]})]
  458. end,
  459. couch_util:to_hex(couch_util:md5(term_to_binary(Base2))).
  460. filter_code(Filter, Props, UserCtx) ->
  461. {DDocName, FilterName} =
  462. case re:run(Filter, "(.*?)/(.*)", [{capture, [1, 2], binary}]) of
  463. {match, [DDocName0, FilterName0]} ->
  464. {DDocName0, FilterName0};
  465. _ ->
  466. throw({error, <<"Invalid filter. Must match `ddocname/filtername`.">>})
  467. end,
  468. ProxyParams = parse_proxy_params(
  469. couch_util:get_value(<<"proxy">>, Props, [])),
  470. DbName = couch_util:get_value(<<"source">>, Props),
  471. Source = try
  472. open_db(DbName, UserCtx, ProxyParams)
  473. catch
  474. _Tag:DbError ->
  475. DbErrorMsg = io_lib:format("Could not open source database `~s`: ~s",
  476. [couch_util:url_strip_password(DbName), couch_util:to_binary(DbError)]),
  477. throw({error, iolist_to_binary(DbErrorMsg)})
  478. end,
  479. try
  480. Body = case (catch open_doc(Source, <<"_design/", DDocName/binary>>)) of
  481. {ok, #doc{body = Body0}} ->
  482. Body0;
  483. DocError ->
  484. DocErrorMsg = io_lib:format(
  485. "Couldn't open document `_design/~s` from source "
  486. "database `~s`: ~s",
  487. [DDocName, dbname(Source), couch_util:to_binary(DocError)]),
  488. throw({error, iolist_to_binary(DocErrorMsg)})
  489. end,
  490. Code = couch_util:get_nested_json_value(
  491. Body, [<<"filters">>, FilterName]),
  492. re:replace(Code, "^\s*(.*?)\s*$", "\\1", [{return, binary}])
  493. after
  494. close_db(Source)
  495. end.
  496. maybe_add_trailing_slash(Url) ->
  497. re:replace(Url, "[^/]$", "&/", [{return, list}]).
  498. get_rep_endpoint(_UserCtx, {Props}) ->
  499. Url = maybe_add_trailing_slash(couch_util:get_value(<<"url">>, Props)),
  500. {BinHeaders} = couch_util:get_value(<<"headers">>, Props, {[]}),
  501. {Auth} = couch_util:get_value(<<"auth">>, Props, {[]}),
  502. case couch_util:get_value(<<"oauth">>, Auth) of
  503. undefined ->
  504. {remote, Url, [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders]};
  505. {OAuth} ->
  506. {remote, Url, [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders], OAuth}
  507. end;
  508. get_rep_endpoint(_UserCtx, <<"http://",_/binary>>=Url) ->
  509. {remote, maybe_add_trailing_slash(Url), []};
  510. get_rep_endpoint(_UserCtx, <<"https://",_/binary>>=Url) ->
  511. {remote, maybe_add_trailing_slash(Url), []};
  512. get_rep_endpoint(UserCtx, <<DbName/binary>>) ->
  513. {local, DbName, UserCtx}.
  514. find_replication_logs(DbList, RepId, RepProps, UserCtx) ->
  515. LogId = ?l2b(?LOCAL_DOC_PREFIX ++ RepId),
  516. fold_replication_logs(DbList, ?REP_ID_VERSION,
  517. LogId, LogId, RepProps, UserCtx, []).
  518. % Accumulate the replication logs
  519. % Falls back to older log document ids and migrates them
  520. fold_replication_logs([], _Vsn, _LogId, _NewId, _RepProps, _UserCtx, Acc) ->
  521. lists:reverse(Acc);
  522. fold_replication_logs([Db|Rest]=Dbs, Vsn, LogId, NewId,
  523. RepProps, UserCtx, Acc) ->
  524. case open_replication_log(Db, LogId) of
  525. {error, not_found} when Vsn > 1 ->
  526. OldRepId = make_replication_id(RepProps, UserCtx, Vsn - 1),
  527. fold_replication_logs(Dbs, Vsn - 1,
  528. ?l2b(?LOCAL_DOC_PREFIX ++ OldRepId), NewId, RepProps, UserCtx, Acc);
  529. {error, not_found} ->
  530. fold_replication_logs(Rest, ?REP_ID_VERSION, NewId, NewId,
  531. RepProps, UserCtx, [#doc{id=NewId}|Acc]);
  532. {ok, Doc} when LogId =:= NewId ->
  533. fold_replication_logs(Rest, ?REP_ID_VERSION, NewId, NewId,
  534. RepProps, UserCtx, [Doc|Acc]);
  535. {ok, Doc} ->
  536. MigratedLog = #doc{id=NewId,body=Doc#doc.body},
  537. fold_replication_logs(Rest, ?REP_ID_VERSION, NewId, NewId,
  538. RepProps, UserCtx, [MigratedLog|Acc])
  539. end.
  540. open_replication_log(Db, DocId) ->
  541. case open_doc(Db, DocId) of
  542. {ok, Doc} ->
  543. ?LOG_DEBUG("found a replication log for ~s", [dbname(Db)]),
  544. {ok, Doc};
  545. _ ->
  546. ?LOG_DEBUG("didn't find a replication log for ~s", [dbname(Db)]),
  547. {error, not_found}
  548. end.
  549. open_doc(#http_db{} = Db, DocId) ->
  550. Req = Db#http_db{resource = couch_util:encode_doc_id(DocId)},
  551. case couch_rep_httpc:request(Req) of
  552. {[{<<"error">>, _}, {<<"reason">>, _}]} ->
  553. {error, not_found};
  554. Doc ->
  555. {ok, couch_doc:from_json_obj(Doc)}
  556. end;
  557. open_doc(Db, DocId) ->
  558. couch_db:open_doc(Db, DocId).
  559. open_db(Props, UserCtx, ProxyParams) ->
  560. open_db(Props, UserCtx, ProxyParams, false).
  561. open_db({Props}, _UserCtx, ProxyParams, CreateTarget) ->
  562. Url = maybe_add_trailing_slash(couch_util:get_value(<<"url">>, Props)),
  563. {AuthProps} = couch_util:get_value(<<"auth">>, Props, {[]}),
  564. {BinHeaders} = couch_util:get_value(<<"headers">>, Props, {[]}),
  565. Headers = [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders],
  566. DefaultHeaders = (#http_db{})#http_db.headers,
  567. Db1 = #http_db{
  568. url = Url,
  569. auth = AuthProps,
  570. headers = lists:ukeymerge(1, Headers, DefaultHeaders)
  571. },
  572. Db = Db1#http_db{
  573. options = Db1#http_db.options ++ ProxyParams ++
  574. couch_rep_httpc:ssl_options(Db1)
  575. },
  576. couch_rep_httpc:db_exists(Db, CreateTarget);
  577. open_db(<<"http://",_/binary>>=Url, _, ProxyParams, CreateTarget) ->
  578. open_db({[{<<"url">>,Url}]}, [], ProxyParams, CreateTarget);
  579. open_db(<<"https://",_/binary>>=Url, _, ProxyParams, CreateTarget) ->
  580. open_db({[{<<"url">>,Url}]}, [], ProxyParams, CreateTarget);
  581. open_db(<<DbName/binary>>, UserCtx, _ProxyParams, CreateTarget) ->
  582. try
  583. case CreateTarget of
  584. true ->
  585. ok = couch_httpd:verify_is_server_admin(UserCtx),
  586. couch_server:create(DbName, [{user_ctx, UserCtx}]);
  587. false ->
  588. ok
  589. end,
  590. case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
  591. {ok, Db} ->
  592. couch_db:monitor(Db),
  593. Db;
  594. {not_found, no_db_file} ->
  595. throw({db_not_found, DbName})
  596. end
  597. catch throw:{unauthorized, _} ->
  598. throw({unauthorized, DbName})
  599. end.
  600. schedule_checkpoint(#state{checkpoint_scheduled = nil} = State) ->
  601. Server = self(),
  602. case timer:apply_after(5000, couch_rep, checkpoint, [Server]) of
  603. {ok, TRef} ->
  604. State#state{checkpoint_scheduled = TRef};
  605. Error ->
  606. ?LOG_ERROR("tried to schedule a checkpoint but got ~p", [Error]),
  607. State
  608. end;
  609. schedule_checkpoint(State) ->
  610. State.
  611. do_checkpoint(State) ->
  612. #state{
  613. source = Source,
  614. target = Target,
  615. committed_seq = NewSeqNum,
  616. start_seq = StartSeqNum,
  617. history = OldHistory,
  618. session_id = SessionId,
  619. source_log = SourceLog,
  620. target_log = TargetLog,
  621. rep_starttime = ReplicationStartTime,
  622. src_starttime = SrcInstanceStartTime,
  623. tgt_starttime = TgtInstanceStartTime,
  624. stats = Stats,
  625. init_args = [_RepId, {RepDoc} | _]
  626. } = State,
  627. case commit_to_both(Source, Target, NewSeqNum) of
  628. {SrcInstanceStartTime, TgtInstanceStartTime} ->
  629. ?LOG_INFO("recording a checkpoint for ~s -> ~s at source update_seq ~p",
  630. [dbname(Source), dbname(Target), NewSeqNum]),
  631. EndTime = ?l2b(httpd_util:rfc1123_date()),
  632. StartTime = ?l2b(ReplicationStartTime),
  633. DocsRead = ets:lookup_element(Stats, docs_read, 2),
  634. DocsWritten = ets:lookup_element(Stats, docs_written, 2),
  635. DocWriteFailures = ets:lookup_element(Stats, doc_write_failures, 2),
  636. NewHistoryEntry = {[
  637. {<<"session_id">>, SessionId},
  638. {<<"start_time">>, StartTime},
  639. {<<"end_time">>, EndTime},
  640. {<<"start_last_seq">>, StartSeqNum},
  641. {<<"end_last_seq">>, NewSeqNum},
  642. {<<"recorded_seq">>, NewSeqNum},
  643. {<<"missing_checked">>, ets:lookup_element(Stats, total_revs, 2)},
  644. {<<"missing_found">>, ets:lookup_element(Stats, missing_revs, 2)},
  645. {<<"docs_read">>, DocsRead},
  646. {<<"docs_written">>, DocsWritten},
  647. {<<"doc_write_failures">>, DocWriteFailures}
  648. ]},
  649. BaseHistory = [
  650. {<<"session_id">>, SessionId},
  651. {<<"source_last_seq">>, NewSeqNum},
  652. {<<"replication_id_version">>, ?REP_ID_VERSION}
  653. ] ++ case couch_util:get_value(<<"doc_ids">>, RepDoc) of
  654. undefined ->
  655. [];
  656. DocIds when is_list(DocIds) ->
  657. % backwards compatibility with the result of a replication by
  658. % doc IDs in versions 0.11.x and 1.0.x
  659. [
  660. {<<"start_time">>, StartTime},
  661. {<<"end_time">>, EndTime},
  662. {<<"docs_read">>, DocsRead},
  663. {<<"docs_written">>, DocsWritten},
  664. {<<"doc_write_failures">>, DocWriteFailures}
  665. ]
  666. end,
  667. % limit history to 50 entries
  668. NewRepHistory = {
  669. BaseHistory ++
  670. [{<<"history">>, lists:sublist([NewHistoryEntry | OldHistory], 50)}]
  671. },
  672. try
  673. {SrcRevPos,SrcRevId} =
  674. update_local_doc(Source, SourceLog#doc{body=NewRepHistory}),
  675. {TgtRevPos,TgtRevId} =
  676. update_local_doc(Target, TargetLog#doc{body=NewRepHistory}),
  677. State#state{
  678. checkpoint_scheduled = nil,
  679. checkpoint_history = NewRepHistory,
  680. source_log = SourceLog#doc{revs={SrcRevPos, [SrcRevId]}},
  681. target_log = TargetLog#doc{revs={TgtRevPos, [TgtRevId]}}
  682. }
  683. catch throw:conflict ->
  684. ?LOG_ERROR("checkpoint failure: conflict (are you replicating to "
  685. "yourself?)", []),
  686. State
  687. end;
  688. _Else ->
  689. ?LOG_INFO("rebooting ~s -> ~s from last known replication checkpoint",
  690. [dbname(Source), dbname(Target)]),
  691. #state{
  692. changes_feed = CF,
  693. missing_revs = MR,
  694. reader = Reader,
  695. writer = Writer
  696. } = State,
  697. Pids = [Writer, Reader, MR, CF],
  698. [unlink(Pid) || Pid <- Pids],
  699. [exit(Pid, shutdown) || Pid <- Pids],
  700. close_db(Target),
  701. close_db(Source),
  702. {ok, NewState} = init(State#state.init_args),
  703. NewState#state{listeners=State#state.listeners}
  704. end.
  705. commit_to_both(Source, Target, RequiredSeq) ->
  706. % commit the src async
  707. ParentPid = self(),
  708. SrcCommitPid = spawn_link(fun() ->
  709. ParentPid ! {self(), ensure_full_commit(Source, RequiredSeq)} end),
  710. % commit tgt sync
  711. TargetStartTime = ensure_full_commit(Target),
  712. SourceStartTime =
  713. receive
  714. {SrcCommitPid, Timestamp} ->
  715. Timestamp;
  716. {'EXIT', SrcCommitPid, {http_request_failed, _}} ->
  717. exit(replication_link_failure)
  718. end,
  719. {SourceStartTime, TargetStartTime}.
  720. ensure_full_commit(#http_db{headers = Headers} = Target) ->
  721. Headers1 = [
  722. {"Content-Length", 0} |
  723. couch_util:proplist_apply_field(
  724. {"Content-Type", "application/json"}, Headers)
  725. ],
  726. Req = Target#http_db{
  727. resource = "_ensure_full_commit",
  728. method = post,
  729. headers = Headers1
  730. },
  731. {ResultProps} = couch_rep_httpc:request(Req),
  732. true = couch_util:get_value(<<"ok">>, ResultProps),
  733. couch_util:get_value(<<"instance_start_time">>, ResultProps);
  734. ensure_full_commit(Target) ->
  735. {ok, NewDb} = couch_db:open_int(Target#db.name, []),
  736. UpdateSeq = couch_db:get_update_seq(Target),
  737. CommitSeq = couch_db:get_committed_update_seq(NewDb),
  738. InstanceStartTime = NewDb#db.instance_start_time,
  739. couch_db:close(NewDb),
  740. if UpdateSeq > CommitSeq ->
  741. ?LOG_DEBUG("target needs a full commit: update ~p commit ~p",
  742. [UpdateSeq, CommitSeq]),
  743. {ok, DbStartTime} = couch_db:ensure_full_commit(Target),
  744. DbStartTime;
  745. true ->
  746. ?LOG_DEBUG("target doesn't need a full commit", []),
  747. InstanceStartTime
  748. end.
  749. ensure_full_commit(#http_db{headers = Headers} = Source, RequiredSeq) ->
  750. Headers1 = [
  751. {"Content-Length", 0} |
  752. couch_util:proplist_apply_field(
  753. {"Content-Type", "application/json"}, Headers)
  754. ],
  755. Req = Source#http_db{
  756. resource = "_ensure_full_commit",
  757. method = post,
  758. qs = [{seq, case RequiredSeq of Bin when is_binary(Bin) -> Bin;
  759. Else -> iolist_to_binary(?JSON_ENCODE(Else)) end}],
  760. headers = Headers1
  761. },
  762. {ResultProps} = couch_rep_httpc:request(Req),
  763. case couch_util:get_value(<<"ok">>, ResultProps) of
  764. true ->
  765. couch_util:get_value(<<"instance_start_time">>, ResultProps);
  766. undefined -> nil end;
  767. ensure_full_commit(Source, RequiredSeq) ->
  768. {ok, NewDb} = couch_db:open_int(Source#db.name, []),
  769. CommitSeq = couch_db:get_committed_update_seq(NewDb),
  770. InstanceStartTime = NewDb#db.instance_start_time,
  771. couch_db:close(NewDb),
  772. if RequiredSeq > CommitSeq ->
  773. ?LOG_DEBUG("source needs a full commit: required ~p committed ~p",
  774. [RequiredSeq, CommitSeq]),
  775. {ok, DbStartTime} = couch_db:ensure_full_commit(Source),
  776. DbStartTime;
  777. true ->
  778. ?LOG_DEBUG("source doesn't need a full commit", []),
  779. InstanceStartTime
  780. end.
  781. update_local_doc(#http_db{} = Db, Doc) ->
  782. Req = Db#http_db{
  783. resource = couch_util:encode_doc_id(Doc),
  784. method = put,
  785. body = couch_doc:to_json_obj(Doc, [attachments]),
  786. headers = [{"x-couch-full-commit", "false"} | Db#http_db.headers]
  787. },
  788. {ResponseMembers} = couch_rep_httpc:request(Req),
  789. Rev = couch_util:get_value(<<"rev">>, ResponseMembers),
  790. couch_doc:parse_rev(Rev);
  791. update_local_doc(Db, Doc) ->
  792. {ok, Result} = couch_db:update_doc(Db, Doc, [delay_commit]),
  793. Result.
  794. up_to_date(#http_db{}, _Seq) ->
  795. true;
  796. up_to_date(Source, Seq) ->
  797. {ok, NewDb} = couch_db:open_int(Source#db.name, []),
  798. T = NewDb#db.update_seq == Seq,
  799. couch_db:close(NewDb),
  800. T.
  801. parse_proxy_params(ProxyUrl) when is_binary(ProxyUrl) ->
  802. parse_proxy_params(?b2l(ProxyUrl));
  803. parse_proxy_params([]) ->
  804. [];
  805. parse_proxy_params(ProxyUrl) ->
  806. #url{
  807. host = Host,
  808. port = Port,
  809. username = User,
  810. password = Passwd
  811. } = ibrowse_lib:parse_url(ProxyUrl),
  812. [{proxy_host, Host}, {proxy_port, Port}] ++
  813. case is_list(User) andalso is_list(Passwd) of
  814. false ->
  815. [];
  816. true ->
  817. [{proxy_user, User}, {proxy_password, Passwd}]
  818. end.
  819. source_db_update_notifier(#db{name = DbName}) ->
  820. Server = self(),
  821. {ok, Notifier} = couch_db_update_notifier:start_link(
  822. fun({compacted, DbName1}) when DbName1 =:= DbName ->
  823. ok = gen_server:cast(Server, reopen_source_db);
  824. (_) ->
  825. ok
  826. end),
  827. Notifier;
  828. source_db_update_notifier(_) ->
  829. nil.
  830. target_db_update_notifier(#db{name = DbName}) ->
  831. Server = self(),
  832. {ok, Notifier} = couch_db_update_notifier:start_link(
  833. fun({compacted, DbName1}) when DbName1 =:= DbName ->
  834. ok = gen_server:cast(Server, reopen_target_db);
  835. (_) ->
  836. ok
  837. end),
  838. Notifier;
  839. target_db_update_notifier(_) ->
  840. nil.
  841. update_task(#state{stats=Stats}) ->
  842. Update = [ {Stat, ets:lookup_element(Stats, Stat, 2)} || Stat <-
  843. [total_revs, missing_revs, docs_read, docs_written, doc_write_failures]],
  844. couch_task_status:update(Update).