123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209(** Standby follower driver (#172) implementation. *)openLwt.SyntaxmoduleStore=Granary_store.StoremoduleWal=Granary_storage.WalmodulePager=Granary_storage.Pagertypefollower_mode=|Following|Promotedtypeacked_position={epoch:int64;frame_idx:int}typet={store:Store.t;pager:Pager.t;wal:Wal.t;mutablemode:follower_mode;mutablelast_epoch:int64;mutablelast_frame_idx:int;apply_mutex:Lwt_mutex.t;reader_gate:target:int->unitLwt.t;on_standby_ack:(int->unit)option}letcreate~store~pager~wal?on_standby_ack()={store;pager;wal;mode=Following;last_epoch=0L;last_frame_idx=-1;apply_mutex=Lwt_mutex.create();reader_gate=Store.wait_for_readers_paststore;on_standby_ack};;letmodet=t.modeletacked_positiont={epoch=t.last_epoch;frame_idx=t.last_frame_idx}letppfmtt=Format.fprintffmt"Standby.t { mode = %s; epoch = %Ld; frame_idx = %d }"(matcht.modewith|Following->"Following"|Promoted->"Promoted")t.last_epocht.last_frame_idx;;letpromotet=matcht.modewith|Promoted->Lwt.return_unit|Following->(* Take the apply mutex so any in-flight [apply_frames_epoch_aware] from
the follower loop completes before we drain and reset. Re-check the
mode under the lock: a concurrent [promote] may have won the race. *)Lwt_mutex.with_lockt.apply_mutex(fun()->matcht.modewith|Promoted->Lwt.return_unit|Following->(* Drain buffered committed frames to the main DB before recycling
the WAL — otherwise the fresh engine view opened on promotion
(which recovers from main) would lose every frame applied since
the last epoch-change checkpoint. [checkpoint_wal_to_main] also
bumps the local WAL epoch via [Wal.reset], so locally-generated
writes start with a fresh epoch distinct from the master's. *)let*r=Replication.checkpoint_wal_to_main~wal:t.wal~pager:t.pager~reader_gate:t.reader_gatein(matchrwith|Error(`Apply_errormsg)->Lwt.fail_with("Standby.promote: drain failed: "^msg)|Ok()->t.mode<-Promoted;Store.set_followert.storefalse;t.last_epoch<-Wal.epocht.wal;(* Nothing applied in the new (local) epoch yet — same sentinel
as [create]. *)t.last_frame_idx<--1;Lwt.return_unit));;letstart_followingtstream=matcht.modewith|Promoted->(* Never re-enter follower mode on a promoted node: doing so would leave
the store rejecting writes (the exit clause below only clears follower
mode when still [Following]). Promotion is terminal. *)Lwt.return(Ok())|Following->Store.set_followert.storetrue;let*result=Lwt.catch(fun()->letrecloop()=let*next=Lwt_stream.getstreaminmatchnextwith|None->Lwt.return(Ok())|Someframes->(* [with_lock] releases the mutex even if [apply_frames_epoch_aware]
raises, so a faulting batch can never wedge a later [promote].
Re-check the mode under the lock: a [promote] may have won the
race while we were blocked, in which case the WAL has been
recycled and we must not apply this batch into the promoted
node. *)let*outcome=Lwt_mutex.with_lockt.apply_mutex(fun()->matcht.modewith|Promoted->Lwt.return(`Stop(Ok()))|Following->let*r=Replication.apply_frames_epoch_aware~wal:t.wal~pager:t.pager~last_epoch:t.last_epoch~last_idx:t.last_frame_idx~reader_gate:t.reader_gateframesin(matchrwith|Error_ase->Lwt.return(`Stope)|Ok(epoch,idx)->t.last_epoch<-epoch;t.last_frame_idx<-idx;letacked=Wal.committed_framest.walinStore.set_follower_ack_positiont.store~frames:acked;(matcht.on_standby_ackwith|Somecb->cbacked|None->());Lwt.return`Continue))in(matchoutcomewith|`Stopresult->Lwt.returnresult|`Continue->loop())inloop())(funexn->Lwt.return(Error(`Apply_error(Printexc.to_stringexn))))in(* Clear follower mode on any exit path unless we have been promoted. *)ift.mode=FollowingthenStore.set_followert.storefalse;Lwt.returnresult;;letrebasetsegments=matcht.modewith|Promoted->Lwt.return(Error(`Apply_error"Standby.rebase: cannot rebase a promoted standby"))|Following->(* Take the apply mutex so a rebase never races an in-flight apply from a
(stopped, but possibly still draining) follower loop. *)Lwt_mutex.with_lockt.apply_mutex(fun()->matcht.modewith|Promoted->Lwt.return(Error(`Apply_error"Standby.rebase: cannot rebase a promoted standby"))|Following->(* The application has overwritten the main DB (the pager passed to
[create]) with a fresh, consistent base snapshot from the object
store. The standby's existing WAL index therefore describes a
superseded state and must be DISCARDED — not drained, which would
corrupt the fresh base. [Wal.reset] drops the index and bumps the
local WAL epoch. *)Wal.resett.wal;t.last_epoch<-Wal.epocht.wal;t.last_frame_idx<--1;(* Replay the object-store WAL segments in order through the
epoch-aware apply primitive, threading the position forward. The
segments may span several master epochs; [apply_frames_epoch_aware]
checkpoints the local WAL at each transition (#209), so on
completion the main DB holds the base plus all-but-the-last epoch
and the WAL holds the live tail's epoch — exactly the steady-state
a following standby maintains. *)letrecloop()=let*next=Lwt_stream.getsegmentsinmatchnextwith|None->Lwt.return(Ok{epoch=t.last_epoch;frame_idx=t.last_frame_idx})|Someframes->let*r=Replication.apply_frames_epoch_aware~wal:t.wal~pager:t.pager~last_epoch:t.last_epoch~last_idx:t.last_frame_idx~reader_gate:t.reader_gateframesin(matchrwith|Error_ase->Lwt.returne|Ok(epoch,idx)->t.last_epoch<-epoch;t.last_frame_idx<-idx;letacked=Wal.committed_framest.walinStore.set_follower_ack_positiont.store~frames:acked;(matcht.on_standby_ackwith|Somecb->cbacked|None->());loop())inloop());;