@@ -30,13 +30,17 @@ use std::{
3030
3131use arrow_array:: { RecordBatch , RecordBatchOptions } ;
3232use arrow_schema:: { ArrowError , DataType , Field , FieldRef , Schema } ;
33+ use chrono:: { DateTime , Utc } ;
34+ use itertools:: Itertools ;
3335use tonic:: async_trait;
3436
3537use crate :: {
3638 event:: format:: get_datatype_suffix,
39+ handlers:: http:: modal:: ingest_server:: INGESTOR_META ,
3740 metastore:: { MetastoreError , metastore_traits:: Metastore } ,
3841 option:: Mode ,
3942 parseable:: { GlobalSchema , PARSEABLE } ,
43+ storage:: object_storage:: global_schema_lock_file_path,
4044} ;
4145
4246const MAX_CAS_ATTEMPTS : usize = 16 ;
@@ -250,7 +254,88 @@ pub async fn resolve(
250254 stream_name,
251255 tenant_id,
252256 } ;
253- resolve_backend ( & backend, stream_name, schemas, pinned) . await
257+ // before beginning, acquire lock
258+ // lock file will be- `tenant/stream/.stream/<nodename>-<timestamp>.lock`
259+ let locks = backend
260+ . metastore
261+ . get_global_schema_locks ( stream_name, tenant_id)
262+ . await ?;
263+ if locks. len ( ) == 0 {
264+ // proceed with writing lock file
265+ } else if locks. len ( ) == 1 {
266+ // only one lock file
267+ // check the time of the existing lock file
268+ // and decide whether to backoff or not
269+ let timestamp = DateTime :: from_timestamp_micros (
270+ * & locks[ 0 ] . rsplit ( "-" ) . collect_vec ( ) [ 0 ]
271+ . parse :: < i64 > ( )
272+ . unwrap ( ) ,
273+ )
274+ . unwrap ( ) ;
275+
276+ let mut slept_already = false ;
277+ loop {
278+ // if delta > 3 minutes then delete and proceed
279+ if ( Utc :: now ( ) - timestamp) . num_seconds ( ) > 180 {
280+ tracing:: warn!(
281+ "delta between lock file- {} and now is greater than 3 minutes, deleting and proceeding" ,
282+ locks[ 0 ]
283+ ) ;
284+ let lock_file =
285+ global_schema_lock_file_path ( stream_name, tenant_id) . join ( & locks[ 0 ] ) ;
286+ backend
287+ . metastore
288+ . delete_global_schema_lock ( & lock_file, tenant_id)
289+ . await ;
290+ } else if !slept_already {
291+ // sleep for 5 seconds and try again
292+ // if lock file still present, skip cycle
293+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
294+ slept_already = true ;
295+ } else {
296+ // delta is < 180s and slept already
297+ // check if lock file exists
298+ let locks = backend
299+ . metastore
300+ . get_global_schema_locks ( stream_name, tenant_id)
301+ . await ?;
302+ if locks. is_empty ( ) {
303+ // proceed normally
304+ } else {
305+ // skip current cycle
306+ return Err ( MetastoreError :: LockFileFailure {
307+ message : "Unable to obtain a clean lock file state" . into ( ) ,
308+ } ) ;
309+ }
310+ }
311+ }
312+ } else {
313+ // multiple lock files present, self healing required here
314+ // multiple lock files should only be present in case of multiple failures
315+ // safe to assume that conflict resolution can fail now
316+ // delete all of them maybe?
317+ }
318+ let lock_file_path = global_schema_lock_file_path ( stream_name, tenant_id) . join ( format ! (
319+ "{}-{}.lock" ,
320+ INGESTOR_META
321+ . get( )
322+ . expect( "Ingestor node metadata should exist" )
323+ . node_id,
324+ Utc :: now( ) . timestamp_micros( )
325+ ) ) ;
326+ backend
327+ . metastore
328+ . put_global_schema_lock ( & lock_file_path, tenant_id)
329+ . await ?;
330+
331+ // proceed with resolution
332+ let res = resolve_backend ( & backend, stream_name, schemas, pinned) . await ;
333+
334+ backend
335+ . metastore
336+ . delete_global_schema_lock ( & lock_file_path, tenant_id)
337+ . await ;
338+ res
254339}
255340
256341async fn resolve_backend (
@@ -309,7 +394,7 @@ async fn resolve_backend(
309394}
310395
311396fn schema_error ( message : impl Into < String > ) -> MetastoreError {
312- MetastoreError :: JsonSchemaError {
397+ MetastoreError :: CASConvergenceFailure {
313398 message : message. into ( ) ,
314399 }
315400}
0 commit comments