PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/apps/couch/src/couch_view_group.erl

http://github.com/cloudant/bigcouch
Erlang | 647 lines | 519 code | 69 blank | 59 comment | 22 complexity | a6485a7aeb07d6089676f0223bb762ea 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_view_group).
  13. -behaviour(gen_server).
  14. %% API
  15. -export([start_link/1, request_group/2, trigger_group_update/2, request_group_info/1]).
  16. -export([open_db_group/2, open_temp_group/5, design_doc_to_view_group/1]).
  17. -export([design_root/2, index_file_name/3]).
  18. %% Exports for the compactor
  19. -export([get_index_header_data/1]).
  20. %% gen_server callbacks
  21. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
  22. terminate/2, code_change/3]).
  23. -include("couch_db.hrl").
  24. -record(group_state, {
  25. type,
  26. db_name,
  27. init_args,
  28. group,
  29. updater_pid=nil,
  30. compactor_pid=nil,
  31. waiting_commit=false,
  32. waiting_list=[],
  33. ref_counter=nil
  34. }).
  35. % api methods
  36. request_group(Pid, Seq) ->
  37. ?LOG_DEBUG("request_group {Pid, Seq} ~p", [{Pid, Seq}]),
  38. case gen_server:call(Pid, {request_group, Seq}, infinity) of
  39. {ok, Group, _RefCounter} ->
  40. {ok, Group};
  41. Error ->
  42. ?LOG_DEBUG("request_group Error ~p", [Error]),
  43. throw(Error)
  44. end.
  45. request_group_info(Pid) ->
  46. case gen_server:call(Pid, request_group_info) of
  47. {ok, GroupInfoList} ->
  48. {ok, GroupInfoList};
  49. Error ->
  50. throw(Error)
  51. end.
  52. trigger_group_update(Pid, RequestSeq) ->
  53. gen_server:cast(Pid, {update_group, RequestSeq}).
  54. % from template
  55. start_link(InitArgs) ->
  56. case gen_server:start_link(couch_view_group,
  57. {InitArgs, self(), Ref = make_ref()}, []) of
  58. {ok, Pid} ->
  59. {ok, Pid};
  60. ignore ->
  61. receive
  62. {Ref, Pid, Error} ->
  63. case process_info(self(), trap_exit) of
  64. {trap_exit, true} -> receive {'EXIT', Pid, _} -> ok end;
  65. {trap_exit, false} -> ok
  66. end,
  67. Error
  68. end;
  69. Error ->
  70. Error
  71. end.
  72. % init creates a closure which spawns the appropriate view_updater.
  73. init({{_, DbName, _} = InitArgs, ReturnPid, Ref}) ->
  74. process_flag(trap_exit, true),
  75. case prepare_group(InitArgs, false) of
  76. {ok, Db, #group{fd=Fd, current_seq=Seq}=Group} ->
  77. case Seq > couch_db:get_update_seq(Db) of
  78. true ->
  79. ReturnPid ! {Ref, self(), {error, invalid_view_seq}},
  80. couch_db:close(Db),
  81. ignore;
  82. _ ->
  83. try couch_db:monitor(Db) after couch_db:close(Db) end,
  84. {ok, #group_state{
  85. db_name=DbName,
  86. init_args=InitArgs,
  87. group=Group,
  88. ref_counter=erlang:monitor(process,Fd)}}
  89. end;
  90. Error ->
  91. ReturnPid ! {Ref, self(), Error},
  92. ignore
  93. end.
  94. % There are two sources of messages: couch_view, which requests an up to date
  95. % view group, and the couch_view_updater, which when spawned, updates the
  96. % group and sends it back here. We employ a caching mechanism, so that between
  97. % database writes, we don't have to spawn a couch_view_updater with every view
  98. % request.
  99. % The caching mechanism: each request is submitted with a seq_id for the
  100. % database at the time it was read. We guarantee to return a view from that
  101. % sequence or newer.
  102. % If the request sequence is higher than our current high_target seq, we set
  103. % that as the highest seqence. If the updater is not running, we launch it.
  104. handle_call({request_group, RequestSeq}, From,
  105. #group_state{
  106. db_name=DbName,
  107. group=#group{current_seq=Seq}=Group,
  108. updater_pid=nil,
  109. waiting_list=WaitList
  110. }=State) when RequestSeq > Seq ->
  111. Owner = self(),
  112. Pid = spawn_link(fun()-> couch_view_updater:update(Owner, Group, DbName) end),
  113. {noreply, State#group_state{
  114. updater_pid=Pid,
  115. waiting_list=[{From,RequestSeq}|WaitList]
  116. }, infinity};
  117. % If the request seqence is less than or equal to the seq_id of a known Group,
  118. % we respond with that Group.
  119. handle_call({request_group, RequestSeq}, _From, #group_state{
  120. group = #group{current_seq=GroupSeq} = Group,
  121. ref_counter = RefCounter
  122. } = State) when RequestSeq =< GroupSeq ->
  123. {reply, {ok, Group, RefCounter}, State};
  124. % Otherwise: TargetSeq => RequestSeq > GroupSeq
  125. % We've already initiated the appropriate action, so just hold the response until the group is up to the RequestSeq
  126. handle_call({request_group, RequestSeq}, From,
  127. #group_state{waiting_list=WaitList}=State) ->
  128. {noreply, State#group_state{
  129. waiting_list=[{From, RequestSeq}|WaitList]
  130. }, infinity};
  131. handle_call(request_group_info, _From, State) ->
  132. GroupInfo = get_group_info(State),
  133. {reply, {ok, GroupInfo}, State};
  134. handle_call({start_compact, CompactFun}, _From, #group_state{compactor_pid=nil}
  135. = State) ->
  136. #group_state{
  137. group = #group{name = GroupId, sig = GroupSig} = Group,
  138. init_args = {RootDir, DbName, _}
  139. } = State,
  140. ?LOG_INFO("View index compaction starting for ~s ~s", [DbName, GroupId]),
  141. {ok, Db} = couch_db:open_int(DbName, []),
  142. {ok, Fd} = open_index_file(compact, RootDir, DbName, GroupSig),
  143. NewGroup = reset_file(Db, Fd, DbName, Group),
  144. couch_db:close(Db),
  145. Pid = spawn_link(fun() -> CompactFun(Group, NewGroup, DbName) end),
  146. {reply, {ok, Pid}, State#group_state{compactor_pid = Pid}};
  147. handle_call({start_compact, _}, _From, #group_state{compactor_pid=Pid} = State) ->
  148. %% compact already running, this is a no-op
  149. {reply, {ok, Pid}, State};
  150. handle_call({compact_done, #group{fd=NewFd, current_seq=NewSeq} = NewGroup}, _From,
  151. #group_state{group = #group{current_seq=OldSeq}} = State)
  152. when NewSeq >= OldSeq ->
  153. #group_state{
  154. group = #group{name=GroupId, fd=OldFd, sig=GroupSig},
  155. init_args = {RootDir, DbName, _},
  156. updater_pid = UpdaterPid,
  157. compactor_pid = CompactorPid,
  158. ref_counter = RefCounter
  159. } = State,
  160. ?LOG_INFO("View index compaction complete for ~s ~s", [DbName, GroupId]),
  161. FileName = index_file_name(RootDir, DbName, GroupSig),
  162. CompactName = index_file_name(compact, RootDir, DbName, GroupSig),
  163. ok = couch_file:delete(RootDir, FileName),
  164. ok = file:rename(CompactName, FileName),
  165. %% if an updater is running, kill it and start a new one
  166. NewUpdaterPid =
  167. if is_pid(UpdaterPid) ->
  168. unlink(UpdaterPid),
  169. exit(UpdaterPid, view_compaction_complete),
  170. Owner = self(),
  171. spawn_link(fun()-> couch_view_updater:update(Owner, NewGroup, DbName) end);
  172. true ->
  173. nil
  174. end,
  175. %% cleanup old group
  176. unlink(CompactorPid),
  177. receive {'EXIT', CompactorPid, normal} -> ok after 0 -> ok end,
  178. unlink(OldFd),
  179. erlang:demonitor(RefCounter),
  180. self() ! delayed_commit,
  181. {reply, ok, State#group_state{
  182. group=NewGroup,
  183. ref_counter=erlang:monitor(process,NewFd),
  184. compactor_pid=nil,
  185. updater_pid=NewUpdaterPid
  186. }};
  187. handle_call({compact_done, NewGroup}, _From, State) ->
  188. #group_state{
  189. group = #group{name = GroupId, current_seq = CurrentSeq},
  190. init_args={_RootDir, DbName, _}
  191. } = State,
  192. ?LOG_INFO("View index compaction still behind for ~s ~s -- current: ~p " ++
  193. "compact: ~p", [DbName, GroupId, CurrentSeq, NewGroup#group.current_seq]),
  194. {reply, update, State}.
  195. handle_cast({update_group, RequestSeq},
  196. #group_state{
  197. db_name = DbName,
  198. group=#group{current_seq=Seq}=Group,
  199. updater_pid=nil}=State) when RequestSeq > Seq ->
  200. Owner = self(),
  201. Pid = spawn_link(fun()-> couch_view_updater:update(Owner, Group, DbName) end),
  202. {noreply, State#group_state{updater_pid=Pid}};
  203. handle_cast({update_group, _RequestSeq}, State) ->
  204. {noreply, State};
  205. handle_cast({partial_update, Pid, NewGroup}, #group_state{updater_pid=Pid}
  206. = State) ->
  207. #group_state{
  208. db_name = DbName,
  209. waiting_commit = WaitingCommit
  210. } = State,
  211. NewSeq = NewGroup#group.current_seq,
  212. ?LOG_DEBUG("checkpointing view update at seq ~p for ~s ~s", [NewSeq,
  213. DbName, NewGroup#group.name]),
  214. if not WaitingCommit ->
  215. erlang:send_after(1000, self(), delayed_commit);
  216. true -> ok
  217. end,
  218. {noreply, State#group_state{group=NewGroup, waiting_commit=true}};
  219. handle_cast({partial_update, _, _}, State) ->
  220. %% message from an old (probably pre-compaction) updater; ignore
  221. {noreply, State}.
  222. handle_info(delayed_commit, #group_state{db_name=DbName,group=Group}=State) ->
  223. {ok, Db} = couch_db:open_int(DbName, []),
  224. CommittedSeq = couch_db:get_committed_update_seq(Db),
  225. couch_db:close(Db),
  226. if CommittedSeq >= Group#group.current_seq ->
  227. % save the header
  228. Header = {Group#group.sig, get_index_header_data(Group)},
  229. ok = couch_file:write_header(Group#group.fd, Header),
  230. {noreply, State#group_state{waiting_commit=false}};
  231. true ->
  232. % We can't commit the header because the database seq that's fully
  233. % committed to disk is still behind us. If we committed now and the
  234. % database lost those changes our view could be forever out of sync
  235. % with the database. But a crash before we commit these changes, no big
  236. % deal, we only lose incremental changes since last committal.
  237. erlang:send_after(1000, self(), delayed_commit),
  238. {noreply, State#group_state{waiting_commit=true}}
  239. end;
  240. handle_info({'EXIT', FromPid, {new_group, Group}},
  241. #group_state{db_name=DbName,
  242. updater_pid=UpPid,
  243. ref_counter=RefCounter,
  244. waiting_list=WaitList,
  245. waiting_commit=WaitingCommit}=State) when UpPid == FromPid ->
  246. if not WaitingCommit ->
  247. erlang:send_after(1000, self(), delayed_commit);
  248. true -> ok
  249. end,
  250. case reply_with_group(Group, WaitList, [], RefCounter) of
  251. [] ->
  252. {noreply, State#group_state{waiting_commit=true, waiting_list=[],
  253. group=Group, updater_pid=nil}};
  254. StillWaiting ->
  255. % we still have some waiters, reopen the database and reupdate the index
  256. Owner = self(),
  257. Pid = spawn_link(fun() -> couch_view_updater:update(Owner, Group, DbName) end),
  258. {noreply, State#group_state{waiting_commit=true,
  259. waiting_list=StillWaiting, updater_pid=Pid}}
  260. end;
  261. handle_info({'EXIT', _, {new_group, _}}, State) ->
  262. %% message from an old (probably pre-compaction) updater; ignore
  263. {noreply, State};
  264. handle_info({'EXIT', UpPid, reset},
  265. #group_state{init_args=InitArgs, updater_pid=UpPid} = State) ->
  266. case prepare_group(InitArgs, true) of
  267. {ok, Db, ResetGroup} ->
  268. Owner = self(),
  269. couch_db:close(Db),
  270. Pid = spawn_link(fun() ->
  271. couch_view_updater:update(Owner, ResetGroup, Db#db.name)
  272. end),
  273. {noreply, State#group_state{
  274. updater_pid=Pid,
  275. group=ResetGroup}};
  276. Error ->
  277. {stop, normal, reply_all(State, Error)}
  278. end;
  279. handle_info({'EXIT', _, reset}, State) ->
  280. %% message from an old (probably pre-compaction) updater; ignore
  281. {noreply, State};
  282. handle_info({'EXIT', _FromPid, normal}, State) ->
  283. {noreply, State};
  284. handle_info({'EXIT', FromPid, {{nocatch, Reason}, _Trace}}, State) ->
  285. ?LOG_DEBUG("Uncaught throw() in linked pid: ~p", [{FromPid, Reason}]),
  286. {stop, Reason, State};
  287. handle_info({'EXIT', FromPid, Reason}, State) ->
  288. ?LOG_DEBUG("Exit from linked pid: ~p", [{FromPid, Reason}]),
  289. {stop, Reason, State};
  290. handle_info({'DOWN',_,_,Pid,Reason}, #group_state{group=G}=State) ->
  291. ?LOG_INFO("Shutting down group server ~p, db ~p closing w/ reason~n~p",
  292. [G#group.name, Pid, Reason]),
  293. {stop, normal, reply_all(State, shutdown)}.
  294. terminate(Reason, #group_state{updater_pid=Update, compactor_pid=Compact}=S) ->
  295. reply_all(S, Reason),
  296. couch_util:shutdown_sync(Update),
  297. couch_util:shutdown_sync(Compact),
  298. ok.
  299. code_change(_OldVsn, State, _Extra) ->
  300. {ok, State}.
  301. %% Local Functions
  302. % reply_with_group/3
  303. % for each item in the WaitingList {Pid, Seq}
  304. % if the Seq is =< GroupSeq, reply
  305. reply_with_group(Group=#group{current_seq=GroupSeq}, [{Pid, Seq}|WaitList],
  306. StillWaiting, RefCounter) when Seq =< GroupSeq ->
  307. gen_server:reply(Pid, {ok, Group, RefCounter}),
  308. reply_with_group(Group, WaitList, StillWaiting, RefCounter);
  309. % else
  310. % put it in the continuing waiting list
  311. reply_with_group(Group, [{Pid, Seq}|WaitList], StillWaiting, RefCounter) ->
  312. reply_with_group(Group, WaitList, [{Pid, Seq}|StillWaiting], RefCounter);
  313. % return the still waiting list
  314. reply_with_group(_Group, [], StillWaiting, _RefCounter) ->
  315. StillWaiting.
  316. reply_all(#group_state{waiting_list=WaitList}=State, Reply) ->
  317. [catch gen_server:reply(Pid, Reply) || {Pid, _} <- WaitList],
  318. State#group_state{waiting_list=[]}.
  319. prepare_group({RootDir, DbName, #group{sig=Sig}=Group}, ForceReset)->
  320. case couch_db:open_int(DbName, []) of
  321. {ok, Db} ->
  322. case open_index_file(RootDir, DbName, Sig) of
  323. {ok, Fd} ->
  324. if ForceReset ->
  325. % this can happen if we missed a purge
  326. {ok, Db, reset_file(Db, Fd, DbName, Group)};
  327. true ->
  328. % 09 UPGRADE CODE
  329. ok = couch_file:upgrade_old_header(Fd, <<$r, $c, $k, 0>>),
  330. case (catch couch_file:read_header(Fd)) of
  331. {ok, {Sig, HeaderInfo}} ->
  332. % sigs match!
  333. {ok, Db, init_group(Db, Fd, Group, HeaderInfo)};
  334. _ ->
  335. % this happens on a new file
  336. {ok, Db, reset_file(Db, Fd, DbName, Group)}
  337. end
  338. end;
  339. {error, Reason} = Error ->
  340. ?LOG_ERROR("Failed to open view file '~s': ~s", [
  341. index_file_name(RootDir, DbName, Sig),
  342. file:format_error(Reason)
  343. ]),
  344. Error
  345. end;
  346. Else ->
  347. Else
  348. end.
  349. get_index_header_data(#group{current_seq=Seq, purge_seq=PurgeSeq,
  350. id_btree=IdBtree,views=Views}) ->
  351. ViewStates = [
  352. {couch_btree:get_state(V#view.btree), V#view.update_seq, V#view.purge_seq} || V <- Views
  353. ],
  354. #index_header{
  355. seq=Seq,
  356. purge_seq=PurgeSeq,
  357. id_btree_state=couch_btree:get_state(IdBtree),
  358. view_states=ViewStates
  359. }.
  360. hex_sig(GroupSig) ->
  361. couch_util:to_hex(?b2l(GroupSig)).
  362. design_root(RootDir, DbName) ->
  363. RootDir ++ "/." ++ ?b2l(DbName) ++ "_design/".
  364. index_file_name(RootDir, DBName, Pid) when is_pid(Pid) ->
  365. {ok, GroupInfo} = request_group_info(Pid),
  366. GroupSig = couch_util:from_hex(couch_util:get_value(signature, GroupInfo)),
  367. index_file_name(RootDir, DBName, GroupSig);
  368. index_file_name(RootDir, DbName, GroupSig) ->
  369. design_root(RootDir, DbName) ++ hex_sig(GroupSig) ++".view".
  370. index_file_name(compact, RootDir, DbName, GroupSig) ->
  371. design_root(RootDir, DbName) ++ hex_sig(GroupSig) ++".compact.view".
  372. open_index_file(RootDir, DbName, GroupSig) ->
  373. FileName = index_file_name(RootDir, DbName, GroupSig),
  374. case couch_file:open(FileName) of
  375. {ok, Fd} -> {ok, Fd};
  376. {error, enoent} -> couch_file:open(FileName, [create]);
  377. Error -> Error
  378. end.
  379. open_index_file(compact, RootDir, DbName, GroupSig) ->
  380. FileName = index_file_name(compact, RootDir, DbName, GroupSig),
  381. case couch_file:open(FileName) of
  382. {ok, Fd} -> {ok, Fd};
  383. {error, enoent} -> couch_file:open(FileName, [create]);
  384. Error -> Error
  385. end.
  386. open_temp_group(DbName, Language, DesignOptions, MapSrc, RedSrc) ->
  387. case couch_db:open_int(DbName, []) of
  388. {ok, Db} ->
  389. View = #view{map_names=[<<"_temp">>],
  390. id_num=0,
  391. btree=nil,
  392. def=MapSrc,
  393. reduce_funs= if RedSrc==[] -> []; true -> [{<<"_temp">>, RedSrc}] end,
  394. options=DesignOptions},
  395. couch_db:close(Db),
  396. {ok, set_view_sig(#group{name = <<"_temp">>,lib={[]}, views=[View],
  397. def_lang=Language, design_options=DesignOptions})};
  398. Error ->
  399. Error
  400. end.
  401. set_view_sig(#group{
  402. views=Views,
  403. lib={[]},
  404. def_lang=Language,
  405. design_options=DesignOptions}=G) ->
  406. ViewInfo = [old_view_format(V) || V <- Views],
  407. G#group{sig=couch_util:md5(term_to_binary({ViewInfo, Language, DesignOptions}))};
  408. set_view_sig(#group{
  409. views=Views,
  410. lib=Lib,
  411. def_lang=Language,
  412. design_options=DesignOptions}=G) ->
  413. ViewInfo = [old_view_format(V) || V <- Views],
  414. G#group{sig=couch_util:md5(term_to_binary({ViewInfo, Language, DesignOptions, sort_lib(Lib)}))}.
  415. % Use the old view record format so group sig's don't change
  416. old_view_format(View) ->
  417. {
  418. view,
  419. View#view.id_num,
  420. View#view.map_names,
  421. View#view.def,
  422. View#view.btree,
  423. View#view.reduce_funs,
  424. View#view.options
  425. }.
  426. sort_lib({Lib}) ->
  427. sort_lib(Lib, []).
  428. sort_lib([], LAcc) ->
  429. lists:keysort(1, LAcc);
  430. sort_lib([{LName, {LObj}}|Rest], LAcc) ->
  431. LSorted = sort_lib(LObj, []), % descend into nested object
  432. sort_lib(Rest, [{LName, LSorted}|LAcc]);
  433. sort_lib([{LName, LCode}|Rest], LAcc) ->
  434. sort_lib(Rest, [{LName, LCode}|LAcc]).
  435. open_db_group(DbName, GroupId) ->
  436. {Pid, Ref} = spawn_monitor(fun() ->
  437. exit(try
  438. fabric:open_doc(mem3:dbname(DbName), GroupId, [])
  439. catch error:database_does_not_exist ->
  440. {ok, Db} = couch_db:open(DbName, []),
  441. couch_db:open_doc(Db, GroupId)
  442. end)
  443. end),
  444. receive {'DOWN', Ref, process, Pid, {ok, Doc}} ->
  445. {ok, design_doc_to_view_group(Doc)};
  446. {'DOWN', Ref, process, Pid, Error} ->
  447. Error
  448. end.
  449. get_group_info(State) ->
  450. #group_state{
  451. group=Group,
  452. updater_pid=UpdaterPid,
  453. compactor_pid=CompactorPid,
  454. waiting_commit=WaitingCommit,
  455. waiting_list=WaitersList
  456. } = State,
  457. #group{
  458. fd = Fd,
  459. sig = GroupSig,
  460. def_lang = Lang,
  461. views = Views,
  462. current_seq=CurrentSeq,
  463. purge_seq=PurgeSeq
  464. } = Group,
  465. {ok, Size} = couch_file:bytes(Fd),
  466. [
  467. {signature, ?l2b(hex_sig(GroupSig))},
  468. {language, Lang},
  469. {disk_size, Size},
  470. {data_size, compute_data_size(Views)},
  471. {updater_running, UpdaterPid /= nil},
  472. {compact_running, CompactorPid /= nil},
  473. {waiting_commit, WaitingCommit},
  474. {waiting_clients, length(WaitersList)},
  475. {update_seq, CurrentSeq},
  476. {purge_seq, PurgeSeq}
  477. ].
  478. compute_data_size(ViewList) ->
  479. lists:foldl(fun(#view{btree=Btree}, Acc) ->
  480. {ok, {_, _, Size}} = couch_btree:full_reduce(Btree),
  481. Size + Acc
  482. end, 0, ViewList).
  483. % maybe move to another module
  484. design_doc_to_view_group(#doc{id=Id,body={Fields}}) ->
  485. Language = couch_util:get_value(<<"language">>, Fields, <<"javascript">>),
  486. {DesignOptions} = couch_util:get_value(<<"options">>, Fields, {[]}),
  487. {RawViews} = couch_util:get_value(<<"views">>, Fields, {[]}),
  488. Lib = couch_util:get_value(<<"lib">>, RawViews, {[]}),
  489. % add the views to a dictionary object, with the map source as the key
  490. DictBySrc =
  491. lists:foldl(
  492. fun({Name, {MRFuns}}, DictBySrcAcc) ->
  493. case couch_util:get_value(<<"map">>, MRFuns) of
  494. undefined -> DictBySrcAcc;
  495. MapSrc ->
  496. RedSrc = couch_util:get_value(<<"reduce">>, MRFuns, null),
  497. {ViewOptions} = couch_util:get_value(<<"options">>, MRFuns, {[]}),
  498. View =
  499. case dict:find({MapSrc, ViewOptions}, DictBySrcAcc) of
  500. {ok, View0} -> View0;
  501. error -> #view{def=MapSrc, options=ViewOptions} % create new view object
  502. end,
  503. View2 =
  504. if RedSrc == null ->
  505. View#view{map_names=[Name|View#view.map_names]};
  506. true ->
  507. View#view{reduce_funs=[{Name,RedSrc}|View#view.reduce_funs]}
  508. end,
  509. dict:store({MapSrc, ViewOptions}, View2, DictBySrcAcc)
  510. end
  511. end, dict:new(), RawViews),
  512. % number the views
  513. {Views, _N} = lists:mapfoldl(
  514. fun({_Src, View}, N) ->
  515. {View#view{id_num=N},N+1}
  516. end, 0, lists:sort(dict:to_list(DictBySrc))),
  517. set_view_sig(#group{name=Id, lib=Lib, views=Views, def_lang=Language, design_options=DesignOptions}).
  518. reset_group(#group{views=Views}=Group) ->
  519. Views2 = [View#view{btree=nil} || View <- Views],
  520. Group#group{fd=nil,query_server=nil,current_seq=0,
  521. id_btree=nil,views=Views2}.
  522. reset_file(Db, Fd, DbName, #group{sig=Sig,name=Name} = Group) ->
  523. ?LOG_DEBUG("Resetting group index \"~s\" in db ~s", [Name, DbName]),
  524. ok = couch_file:truncate(Fd, 0),
  525. ok = couch_file:write_header(Fd, {Sig, nil}),
  526. init_group(Db, Fd, reset_group(Group), nil).
  527. init_group(Db, Fd, #group{views=Views}=Group, nil) ->
  528. init_group(Db, Fd, Group,
  529. #index_header{seq=0, purge_seq=couch_db:get_purge_seq(Db),
  530. id_btree_state=nil, view_states=[{nil, 0, 0} || _ <- Views]});
  531. init_group(_Db, Fd, #group{def_lang=Lang,views=Views}=
  532. Group, IndexHeader) ->
  533. #index_header{seq=Seq, purge_seq=PurgeSeq,
  534. id_btree_state=IdBtreeState, view_states=ViewStates} = IndexHeader,
  535. StateUpdate = fun
  536. ({_, _, _}=State) -> State;
  537. (State) -> {State, 0, 0}
  538. end,
  539. ViewStates2 = lists:map(StateUpdate, ViewStates),
  540. {ok, IdBtree} = couch_btree:open(IdBtreeState, Fd),
  541. Views2 = lists:zipwith(
  542. fun({BTState, USeq, PSeq}, #view{reduce_funs=RedFuns,options=Options}=View) ->
  543. FunSrcs = [FunSrc || {_Name, FunSrc} <- RedFuns],
  544. ReduceFun =
  545. fun(reduce, KVs) ->
  546. KVs2 = couch_view:expand_dups(KVs,[]),
  547. KVs3 = couch_view:detuple_kvs(KVs2,[]),
  548. {ok, Reduced} = couch_query_servers:reduce(Lang, FunSrcs,
  549. KVs3),
  550. {length(KVs3), Reduced, couch_view:data_size(KVs3, Reduced)};
  551. (rereduce, Reds) ->
  552. Count = lists:sum(extract(Reds, counts)),
  553. DataSize = lists:sum(extract(Reds, data_size)),
  554. UserReds = extract(Reds, user_reds),
  555. {ok, Reduced} = couch_query_servers:rereduce(Lang, FunSrcs,
  556. UserReds),
  557. {Count, Reduced, DataSize}
  558. end,
  559. case couch_util:get_value(<<"collation">>, Options, <<"default">>) of
  560. <<"default">> ->
  561. Less = fun couch_view:less_json_ids/2;
  562. <<"raw">> ->
  563. Less = fun(A,B) -> A < B end
  564. end,
  565. {ok, Btree} = couch_btree:open(BTState, Fd,
  566. [{less, Less}, {reduce, ReduceFun}]
  567. ),
  568. View#view{btree=Btree, update_seq=USeq, purge_seq=PSeq}
  569. end,
  570. ViewStates2, Views),
  571. Group#group{fd=Fd, current_seq=Seq, purge_seq=PurgeSeq, id_btree=IdBtree,
  572. views=Views2}.
  573. extract(Reds, counts) ->
  574. [element(1, R) || R <- Reds];
  575. extract(Reds, user_reds) ->
  576. [element(2, R) || R <- Reds];
  577. extract(Reds, data_size) ->
  578. lists:map(fun({_, _}) -> 0; ({_, _, Size}) -> Size end, Reds).