@@ -18,6 +18,8 @@ use commit_id::print_commit_id;
1818use context:: CoreContext ;
1919use futures:: stream:: TryStreamExt ;
2020use metaconfig_types:: RepoConfigRef ;
21+ use mononoke_types:: ChangesetId ;
22+ use mononoke_types:: DateTime ;
2123use repo_identity:: RepoIdentityRef ;
2224use tokio:: process:: Command ;
2325
@@ -41,6 +43,10 @@ const AWS_K8S_NAMESPACE: &str = "default";
4143const AWS_DEPLOYMENT : & str = "mononoke-server" ;
4244const AWS_CONTAINER : & str = "server" ;
4345
46+ // How many recent prod bookmark moves to scan when locating the AWS shadow's
47+ // current changeset to time how far behind it is.
48+ const BEHIND_LOOKBACK : u32 = 1000 ;
49+
4450#[ derive( Args ) ]
4551pub struct StatusArgs {
4652 /// The bookmark modern sync mirrors (it only syncs this one bookmark)
@@ -78,7 +84,15 @@ pub async fn status(ctx: &CoreContext, repo: &Repo, args: StatusArgs) -> Result<
7884 Some ( cs_id) => print_commit_id ( ctx, repo, BONSAI , cs_id) . await ?,
7985 None => println ! ( "(not set)" ) ,
8086 }
81- print_aws_value ( kubeconfig_ok, & shadow_repo, & [ "get" , & bookmark] ) . await ;
87+ let aws_master = print_aws_value ( kubeconfig_ok, & shadow_repo, & [ "get" , & bookmark] ) . await ;
88+ print_behind (
89+ ctx,
90+ repo,
91+ & args. bookmark ,
92+ internal_master,
93+ aws_master. as_deref ( ) ,
94+ )
95+ . await ?;
8296 println ! ( ) ;
8397
8498 // --- latest movement (internal vs AWS) ---
@@ -134,6 +148,87 @@ async fn print_internal_latest_movement(
134148 Ok ( ( ) )
135149}
136150
151+ /// Print how far the AWS shadow's bookmark is behind the internal repo.
152+ ///
153+ /// We use the server-assigned `bookmark_update_log` timestamps (monotonic), not
154+ /// a changeset's client-provided author date which can be skewed. The gap is
155+ /// between when the internal repo last moved the bookmark and when it moved the
156+ /// bookmark onto the changeset the shadow currently points at.
157+ async fn print_behind (
158+ ctx : & CoreContext ,
159+ repo : & Repo ,
160+ bookmark : & BookmarkKey ,
161+ internal_master : Option < ChangesetId > ,
162+ aws_master_raw : Option < & str > ,
163+ ) -> Result < ( ) > {
164+ let aws_master = aws_master_raw. and_then ( |s| s. parse :: < ChangesetId > ( ) . ok ( ) ) ;
165+ let ( Some ( internal_master) , Some ( aws_master) ) = ( internal_master, aws_master) else {
166+ println ! ( " behind: unknown (missing a bookmark value)" ) ;
167+ return Ok ( ( ) ) ;
168+ } ;
169+ if internal_master == aws_master {
170+ println ! ( " behind: in sync" ) ;
171+ return Ok ( ( ) ) ;
172+ }
173+
174+ // Newest-first list of recent moves of this bookmark, with server timestamps.
175+ let entries: Vec < _ > = repo
176+ . bookmark_update_log ( )
177+ . list_bookmark_log_entries (
178+ ctx. clone ( ) ,
179+ bookmark. clone ( ) ,
180+ BEHIND_LOOKBACK ,
181+ None ,
182+ Freshness :: MostRecent ,
183+ )
184+ . try_collect ( )
185+ . await
186+ . context ( "Failed to list bookmark log entries" ) ?;
187+
188+ let internal_secs = entries
189+ . first ( )
190+ . map ( |( _, _, _, ts) | DateTime :: from ( * ts) . timestamp_secs ( ) ) ;
191+ let aws_secs = entries
192+ . iter ( )
193+ . find ( |( _, cs_id, _, _) | * cs_id == Some ( aws_master) )
194+ . map ( |( _, _, _, ts) | DateTime :: from ( * ts) . timestamp_secs ( ) ) ;
195+
196+ match ( internal_secs, aws_secs) {
197+ ( Some ( now) , Some ( then) ) if now >= then => {
198+ println ! (
199+ " behind: AWS is {} behind prod" ,
200+ human_duration( now - then)
201+ )
202+ }
203+ ( Some ( now) , Some ( then) ) => println ! (
204+ " behind: AWS bookmark is {} newer than prod (?)" ,
205+ human_duration( then - now)
206+ ) ,
207+ ( Some ( _) , None ) => println ! (
208+ " behind: AWS changeset not in the last {BEHIND_LOOKBACK} prod moves (very stale or diverged)"
209+ ) ,
210+ _ => println ! ( " behind: unknown (no bookmark history)" ) ,
211+ }
212+ Ok ( ( ) )
213+ }
214+
215+ /// Render a non-negative duration in seconds as a coarse "Xd Yh" / "Xh Ym" string.
216+ fn human_duration ( secs : i64 ) -> String {
217+ let days = secs / 86400 ;
218+ let hours = ( secs % 86400 ) / 3600 ;
219+ let mins = ( secs % 3600 ) / 60 ;
220+ let seconds = secs % 60 ;
221+ if days > 0 {
222+ format ! ( "{days}d {hours}h" )
223+ } else if hours > 0 {
224+ format ! ( "{hours}h {mins}m" )
225+ } else if mins > 0 {
226+ format ! ( "{mins}m {seconds}s" )
227+ } else {
228+ format ! ( "{seconds}s" )
229+ }
230+ }
231+
137232/// Point kubectl at the AWS shadow cluster. Returns false (and prints a note) if
138233/// the `cloud` CLI is missing or fails, so the rest of the report still shows.
139234async fn ensure_aws_kubeconfig ( ) -> bool {
0 commit comments