@@ -30,13 +30,18 @@ use std::{
3030
3131use arrow_array:: { RecordBatch , RecordBatchOptions } ;
3232use arrow_schema:: { ArrowError , DataType , Field , FieldRef , Schema } ;
33+ use chrono:: { DateTime , Utc } ;
34+ use futures:: stream:: FuturesUnordered ;
35+ use itertools:: Itertools ;
3336use tonic:: async_trait;
3437
3538use crate :: {
3639 event:: format:: get_datatype_suffix,
40+ handlers:: http:: modal:: ingest_server:: INGESTOR_META ,
3741 metastore:: { MetastoreError , metastore_traits:: Metastore } ,
3842 option:: Mode ,
3943 parseable:: { GlobalSchema , PARSEABLE } ,
44+ storage:: object_storage:: global_schema_lock_file_path,
4045} ;
4146
4247const MAX_CAS_ATTEMPTS : usize = 16 ;
@@ -250,7 +255,101 @@ pub async fn resolve(
250255 stream_name,
251256 tenant_id,
252257 } ;
253- resolve_backend ( & backend, stream_name, schemas, pinned) . await
258+ // before beginning, acquire lock
259+ // lock file will be- `tenant/stream/.stream/<nodename>-<timestamp>.lock`
260+ let locks = backend
261+ . metastore
262+ . get_global_schema_locks ( stream_name, tenant_id)
263+ . await ?;
264+ if locks. len ( ) == 0 {
265+ // proceed with writing lock file
266+ } else if locks. len ( ) == 1 {
267+ // only one lock file
268+ // check the time of the existing lock file
269+ // and decide whether to backoff or not
270+ let timestamp = DateTime :: from_timestamp_micros (
271+ * & locks[ 0 ] . rsplit ( "-" ) . collect_vec ( ) [ 0 ]
272+ . split_once ( ".lock" )
273+ . unwrap ( )
274+ . 0
275+ . parse :: < i64 > ( )
276+ . unwrap ( ) ,
277+ )
278+ . unwrap ( ) ;
279+
280+ let mut slept_already = false ;
281+ loop {
282+ // if delta > 3 minutes then delete and proceed
283+ if ( Utc :: now ( ) - timestamp) . num_seconds ( ) > 180 {
284+ tracing:: warn!(
285+ "delta between lock file- {} and now is greater than 3 minutes, deleting and proceeding" ,
286+ locks[ 0 ]
287+ ) ;
288+ let lock_file =
289+ global_schema_lock_file_path ( stream_name, tenant_id) . join ( & locks[ 0 ] ) ;
290+ backend
291+ . metastore
292+ . delete_global_schema_lock ( lock_file, tenant_id)
293+ . await ;
294+ break ;
295+ } else if !slept_already {
296+ // sleep for 5 seconds and try again
297+ // if lock file still present, skip cycle
298+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
299+ slept_already = true ;
300+ } else {
301+ // delta is < 180s and slept already
302+ // check if lock file exists
303+ let locks = backend
304+ . metastore
305+ . get_global_schema_locks ( stream_name, tenant_id)
306+ . await ?;
307+ if locks. is_empty ( ) {
308+ // proceed normally
309+ break ;
310+ } else {
311+ // skip current cycle
312+ return Err ( MetastoreError :: LockFileFailure {
313+ message : "Unable to obtain a clean lock file state" . into ( ) ,
314+ } ) ;
315+ }
316+ }
317+ }
318+ } else {
319+ // multiple lock files present, self healing required here
320+ // multiple lock files should only be present in case of multiple failures
321+ // safe to assume that conflict resolution can fail now
322+ // delete all of them maybe?
323+ tracing:: warn!( "Found {} lock files for global schema" , locks. len( ) ) ;
324+ let tasks = FuturesUnordered :: new ( ) ;
325+ for file in locks {
326+ let path = global_schema_lock_file_path ( stream_name, tenant_id) . join ( file) ;
327+ let deletion = backend. metastore . delete_global_schema_lock ( path, tenant_id) ;
328+ tasks. push ( deletion) ;
329+ }
330+ tasks. iter ( ) . for_each ( |_| { } ) ;
331+ }
332+ let lock_file_path = global_schema_lock_file_path ( stream_name, tenant_id) . join ( format ! (
333+ "{}-{}.lock" ,
334+ INGESTOR_META
335+ . get( )
336+ . expect( "Ingestor node metadata should exist" )
337+ . node_id,
338+ Utc :: now( ) . timestamp_micros( )
339+ ) ) ;
340+ backend
341+ . metastore
342+ . put_global_schema_lock ( lock_file_path. clone ( ) , tenant_id)
343+ . await ?;
344+
345+ // proceed with resolution
346+ let res = resolve_backend ( & backend, stream_name, schemas, pinned) . await ;
347+
348+ backend
349+ . metastore
350+ . delete_global_schema_lock ( lock_file_path, tenant_id)
351+ . await ;
352+ res
254353}
255354
256355async fn resolve_backend (
@@ -309,7 +408,7 @@ async fn resolve_backend(
309408}
310409
311410fn schema_error ( message : impl Into < String > ) -> MetastoreError {
312- MetastoreError :: JsonSchemaError {
411+ MetastoreError :: CASConvergenceFailure {
313412 message : message. into ( ) ,
314413 }
315414}
0 commit comments