Skip to main content

gglib_core/services/
model_verification.rs

1//! Model verification service for integrity checking and update detection.
2//!
3//! This service provides:
4//! - Integrity verification via SHA256 hash comparison against `HuggingFace` OIDs
5//! - Update detection by comparing local OIDs with remote repository state
6//! - Model repair by re-downloading corrupt or missing shards
7//! - Concurrency control to prevent conflicting operations on the same model
8
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::Read;
12use std::path::Path;
13use std::sync::Arc;
14
15use async_trait::async_trait;
16use chrono::Utc;
17use serde::{Deserialize, Serialize};
18use sha2::{Digest, Sha256};
19use tokio::sync::{RwLock, mpsc};
20use tokio::task::JoinHandle;
21
22use crate::domain::ModelFile;
23use crate::ports::{HfClientPort, ModelRepository, RepositoryError};
24
25// ============================================================================
26// Domain Types
27// ============================================================================
28
29/// Progress status for an individual shard verification.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "status", rename_all = "snake_case")]
32pub enum ShardProgress {
33    /// Verification starting for this shard.
34    Starting,
35    /// Currently hashing the file.
36    Hashing {
37        /// Percentage complete (0-100).
38        percent: u8,
39        /// Bytes processed so far.
40        bytes_processed: u64,
41        /// Total bytes in the file.
42        total_bytes: u64,
43    },
44    /// Verification completed for this shard.
45    Completed {
46        /// Health status of this shard.
47        health: ShardHealth,
48    },
49}
50
51/// Health status of an individual shard after verification.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(tag = "type", rename_all = "snake_case")]
54#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
55pub enum ShardHealth {
56    /// File is healthy - hash matches expected OID.
57    Healthy,
58    /// File is corrupt - hash doesn't match expected OID.
59    Corrupt {
60        /// Expected SHA256 hash (from `HuggingFace` OID).
61        expected: String,
62        /// Actual computed SHA256 hash.
63        actual: String,
64    },
65    /// File is missing from disk.
66    Missing,
67    /// No OID available to verify against.
68    NoOid,
69}
70
71/// Progress update during model verification.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct VerificationProgress {
74    /// Model ID being verified.
75    pub model_id: i64,
76    /// Current shard index being verified.
77    pub shard_index: usize,
78    /// Total number of shards.
79    pub total_shards: usize,
80    /// Progress status for this shard.
81    pub shard_progress: ShardProgress,
82}
83
84/// Complete verification report for a model.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
87pub struct VerificationReport {
88    /// Model ID that was verified.
89    #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
90    pub model_id: i64,
91    /// Overall health status.
92    pub overall_health: OverallHealth,
93    /// Health status for each shard.
94    pub shards: Vec<ShardHealthReport>,
95    /// When the verification was performed.
96    pub verified_at: chrono::DateTime<Utc>,
97}
98
99/// Overall health status for a model.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
102#[serde(rename_all = "snake_case")]
103pub enum OverallHealth {
104    /// All shards are healthy.
105    Healthy,
106    /// One or more shards are corrupt or missing.
107    Unhealthy,
108    /// No OIDs available for verification.
109    Unverifiable,
110}
111
112/// Health report for a single shard.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
115pub struct ShardHealthReport {
116    /// Shard index.
117    pub index: usize,
118    /// File path.
119    pub file_path: String,
120    /// Health status.
121    pub health: ShardHealth,
122}
123
124/// Result of checking for model updates.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
127pub struct UpdateCheckResult {
128    /// Model ID that was checked.
129    #[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
130    pub model_id: i64,
131    /// Whether an update is available.
132    pub update_available: bool,
133    /// Details about what changed (if update available).
134    pub details: Option<UpdateDetails>,
135}
136
137/// Details about available updates.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
140pub struct UpdateDetails {
141    /// Number of shards that have changed.
142    pub changed_shards: usize,
143    /// OID changes per shard.
144    pub changes: Vec<ShardUpdate>,
145}
146
147/// Update information for a single shard.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS), ts(export))]
150pub struct ShardUpdate {
151    /// Shard index.
152    pub index: usize,
153    /// File path.
154    pub file_path: String,
155    /// Old OID (local).
156    pub old_oid: String,
157    /// New OID (remote).
158    pub new_oid: String,
159}
160
161/// Type of operation being performed on a model.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub(super) enum OperationType {
164    /// Model is being verified.
165    Verifying,
166    /// Model is being downloaded/repaired.
167    Downloading,
168}
169
170// ============================================================================
171// Concurrency Control
172// ============================================================================
173
174/// RAII guard that automatically releases the operation lock when dropped.
175pub(super) struct OperationGuard {
176    model_id: i64,
177    lock_map: Arc<RwLock<HashMap<i64, OperationType>>>,
178}
179
180impl Drop for OperationGuard {
181    fn drop(&mut self) {
182        let model_id = self.model_id;
183        let lock_map: Arc<RwLock<HashMap<i64, OperationType>>> = Arc::clone(&self.lock_map);
184
185        // Spawn a task to release the lock asynchronously
186        tokio::spawn(async move {
187            let mut map = lock_map.write().await;
188            map.remove(&model_id);
189        });
190    }
191}
192
193/// Concurrency control for model operations.
194///
195/// Ensures only one operation of each type can run on a model at a time.
196pub(super) struct ModelOperationLock {
197    locks: Arc<RwLock<HashMap<i64, OperationType>>>,
198}
199
200impl ModelOperationLock {
201    /// Create a new operation lock manager.
202    pub(super) fn new() -> Self {
203        Self {
204            locks: Arc::new(RwLock::new(HashMap::new())),
205        }
206    }
207
208    /// Try to acquire a lock for the specified operation.
209    ///
210    /// Returns `Ok(guard)` if the lock was acquired, or `Err` if another
211    /// operation is already in progress for this model.
212    pub(super) async fn try_acquire(
213        &self,
214        model_id: i64,
215        operation: OperationType,
216    ) -> Result<OperationGuard, String> {
217        let mut map = self.locks.write().await;
218
219        if let Some(existing) = map.get(&model_id) {
220            return Err(format!(
221                "Model {model_id} is already locked for {existing:?} operation"
222            ));
223        }
224
225        map.insert(model_id, operation);
226        drop(map);
227
228        Ok(OperationGuard {
229            model_id,
230            lock_map: Arc::clone(&self.locks),
231        })
232    }
233}
234
235impl Default for ModelOperationLock {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241// ============================================================================
242// Service
243// ============================================================================
244
245/// Port trait for accessing model files repository.
246///
247/// This is a minimal trait that wraps the concrete `ModelFilesRepository`
248/// to avoid circular dependencies.
249#[async_trait]
250pub trait ModelFilesReaderPort: Send + Sync {
251    /// Get all model files for a specific model.
252    async fn get_by_model_id(&self, model_id: i64) -> anyhow::Result<Vec<ModelFile>>;
253
254    /// Update the last verified timestamp for a model file.
255    async fn update_verification_time(
256        &self,
257        id: i64,
258        verified_at: chrono::DateTime<Utc>,
259    ) -> anyhow::Result<()>;
260}
261
262/// Port trait for triggering downloads.
263///
264/// This abstracts the download manager to avoid tight coupling.
265#[async_trait]
266pub trait DownloadTriggerPort: Send + Sync {
267    /// Queue a download for a specific model by repo ID and quantization.
268    async fn queue_download(
269        &self,
270        repo_id: String,
271        quantization: Option<String>,
272    ) -> anyhow::Result<String>;
273}
274
275/// Model verification service.
276pub struct ModelVerificationService {
277    /// Repository for model metadata.
278    model_repo: Arc<dyn ModelRepository>,
279    /// Repository for model file metadata.
280    model_files_repo: Arc<dyn ModelFilesReaderPort>,
281    /// `HuggingFace` client for update checks.
282    hf_client: Arc<dyn HfClientPort>,
283    /// Download trigger for repairs.
284    download_trigger: Arc<dyn DownloadTriggerPort>,
285    /// Concurrency control.
286    operation_lock: ModelOperationLock,
287}
288
289impl ModelVerificationService {
290    /// Create a new verification service.
291    pub fn new(
292        model_repo: Arc<dyn ModelRepository>,
293        model_files_repo: Arc<dyn ModelFilesReaderPort>,
294        hf_client: Arc<dyn HfClientPort>,
295        download_trigger: Arc<dyn DownloadTriggerPort>,
296    ) -> Self {
297        Self {
298            model_repo,
299            model_files_repo,
300            hf_client,
301            download_trigger,
302            operation_lock: ModelOperationLock::new(),
303        }
304    }
305
306    /// Verify the integrity of a model by computing SHA256 hashes.
307    ///
308    /// Returns a channel for progress updates and a handle to the verification task.
309    ///
310    /// # Arguments
311    ///
312    /// * `model_id` - ID of the model to verify
313    ///
314    /// # Returns
315    ///
316    /// * `receiver` - Channel for receiving progress updates
317    /// * `handle` - Join handle for the verification task
318    pub async fn verify_model_integrity(
319        &self,
320        model_id: i64,
321    ) -> Result<
322        (
323            mpsc::Receiver<VerificationProgress>,
324            JoinHandle<Result<VerificationReport, RepositoryError>>,
325        ),
326        String,
327    > {
328        // Acquire lock
329        let guard = self
330            .operation_lock
331            .try_acquire(model_id, OperationType::Verifying)
332            .await?;
333
334        // Get model and file metadata
335        let model = self
336            .model_repo
337            .get_by_id(model_id)
338            .await
339            .map_err(|e| format!("Failed to get model: {e}"))?;
340
341        let model_files = self
342            .model_files_repo
343            .get_by_model_id(model_id)
344            .await
345            .map_err(|e| format!("Failed to get model files: {e}"))?;
346
347        if model_files.is_empty() {
348            return Err("No model files found for verification".to_string());
349        }
350
351        // Get base directory from model's file path
352        let base_dir = model
353            .file_path
354            .parent()
355            .ok_or_else(|| "Failed to get model directory".to_string())?
356            .to_path_buf();
357
358        let total_shards = model_files.len();
359
360        // Create progress channel
361        let (tx, rx) = mpsc::channel(100);
362
363        // Clone dependencies for the async task
364        let model_files_repo = Arc::clone(&self.model_files_repo);
365        let _model_repo = Arc::clone(&self.model_repo);
366
367        // Spawn verification task
368        let handle = tokio::spawn(async move {
369            // Hold the operation lock for the duration of the verification task
370            let _guard = guard;
371            let mut shard_reports = Vec::new();
372            let mut has_unhealthy = false;
373            let mut has_healthy_or_no_oid = false;
374
375            for (index, file) in model_files.iter().enumerate() {
376                // Send starting progress
377                let _ = tx
378                    .send(VerificationProgress {
379                        model_id,
380                        shard_index: index,
381                        total_shards,
382                        shard_progress: ShardProgress::Starting,
383                    })
384                    .await;
385
386                // Resolve file path relative to base directory
387                let resolved_path = base_dir.join(&file.file_path);
388                let health =
389                    Self::verify_shard(file, &resolved_path, model_id, index, total_shards, &tx)
390                        .await;
391
392                // Update verification timestamp
393                if let Err(e) = model_files_repo
394                    .update_verification_time(file.id, Utc::now())
395                    .await
396                {
397                    tracing::warn!(
398                        model_id = model_id,
399                        file_id = file.id,
400                        error = %e,
401                        "Failed to update verification timestamp"
402                    );
403                }
404
405                // Track overall health
406                match &health {
407                    ShardHealth::Corrupt { .. } | ShardHealth::Missing => has_unhealthy = true,
408                    ShardHealth::Healthy | ShardHealth::NoOid => has_healthy_or_no_oid = true,
409                }
410
411                shard_reports.push(ShardHealthReport {
412                    index,
413                    file_path: file.file_path.clone(),
414                    health: health.clone(),
415                });
416
417                // Send completion progress
418                let _ = tx
419                    .send(VerificationProgress {
420                        model_id,
421                        shard_index: index,
422                        total_shards,
423                        shard_progress: ShardProgress::Completed { health },
424                    })
425                    .await;
426            }
427
428            let overall_health = if has_unhealthy {
429                OverallHealth::Unhealthy
430            } else if !has_healthy_or_no_oid {
431                OverallHealth::Unverifiable
432            } else {
433                OverallHealth::Healthy
434            };
435
436            Ok(VerificationReport {
437                model_id,
438                overall_health,
439                shards: shard_reports,
440                verified_at: Utc::now(),
441            })
442        });
443
444        Ok((rx, handle))
445    }
446
447    /// Verify a single shard by computing its SHA256 and comparing with OID.
448    #[allow(clippy::cognitive_complexity)]
449    async fn verify_shard(
450        file: &ModelFile,
451        resolved_path: &Path,
452        model_id: i64,
453        index: usize,
454        total_shards: usize,
455        tx: &mpsc::Sender<VerificationProgress>,
456    ) -> ShardHealth {
457        // Check if OID is available
458        let Some(ref expected_oid) = file.hf_oid else {
459            return ShardHealth::NoOid;
460        };
461
462        // SHA256 hashes are 64 hex characters. If the stored OID is shorter
463        // (e.g. 40 chars = Git SHA-1), it's the wrong hash type and can't be
464        // used for verification.
465        if expected_oid.len() != 64 {
466            tracing::warn!(
467                model_id = model_id,
468                file_path = %file.file_path,
469                oid_len = expected_oid.len(),
470                "Stored OID is not a SHA256 hash (expected 64 hex chars). \
471                 Re-download or update model metadata to fix."
472            );
473            return ShardHealth::NoOid;
474        }
475
476        let file_path = resolved_path;
477
478        // Check if file exists
479        if !file_path.exists() {
480            return ShardHealth::Missing;
481        }
482
483        // Compute SHA256 in a blocking task
484        let path_owned = file_path.to_path_buf();
485        let tx_clone = tx.clone();
486
487        let result = tokio::task::spawn_blocking(move || -> anyhow::Result<String> {
488            let mut file = File::open(&path_owned)?;
489            let total_bytes = file.metadata()?.len();
490
491            let mut hasher = Sha256::new();
492            let mut buffer = vec![0u8; 1024 * 1024]; // 1MB chunks
493            let mut bytes_processed = 0u64;
494
495            // Initial progress
496            let _ = tx_clone.blocking_send(VerificationProgress {
497                model_id,
498                shard_index: index,
499                total_shards,
500                shard_progress: ShardProgress::Hashing {
501                    percent: 0,
502                    bytes_processed: 0,
503                    total_bytes,
504                },
505            });
506
507            loop {
508                let n = file.read(&mut buffer)?;
509                if n == 0 {
510                    break;
511                }
512
513                hasher.update(&buffer[..n]);
514                bytes_processed += n as u64;
515
516                // Report progress every ~100MB or at end
517                if bytes_processed % (100 * 1024 * 1024) < (1024 * 1024)
518                    || bytes_processed == total_bytes
519                {
520                    #[allow(
521                        clippy::cast_possible_truncation,
522                        clippy::cast_precision_loss,
523                        clippy::cast_sign_loss
524                    )]
525                    let percent = ((bytes_processed as f64 / total_bytes as f64) * 100.0) as u8;
526
527                    let _ = tx_clone.blocking_send(VerificationProgress {
528                        model_id,
529                        shard_index: index,
530                        total_shards,
531                        shard_progress: ShardProgress::Hashing {
532                            percent,
533                            bytes_processed,
534                            total_bytes,
535                        },
536                    });
537                }
538            }
539
540            Ok(format!("{:x}", hasher.finalize()))
541        })
542        .await;
543
544        match result {
545            Ok(Ok(computed_hash)) => {
546                if computed_hash == *expected_oid {
547                    ShardHealth::Healthy
548                } else {
549                    ShardHealth::Corrupt {
550                        expected: expected_oid.clone(),
551                        actual: computed_hash,
552                    }
553                }
554            }
555            Ok(Err(e)) => {
556                tracing::error!(
557                    model_id = model_id,
558                    file_path = %file.file_path,
559                    error = %e,
560                    "Failed to compute hash"
561                );
562                ShardHealth::Missing
563            }
564            Err(e) => {
565                tracing::error!(
566                    model_id = model_id,
567                    file_path = %file.file_path,
568                    error = %e,
569                    "Task panicked during hash computation"
570                );
571                ShardHealth::Missing
572            }
573        }
574    }
575
576    /// Check if updates are available for a model.
577    ///
578    /// Compares local OIDs with remote OIDs from `HuggingFace`.
579    pub async fn check_for_updates(
580        &self,
581        model_id: i64,
582    ) -> Result<UpdateCheckResult, RepositoryError> {
583        // Get model metadata
584        let model = self.model_repo.get_by_id(model_id).await?;
585
586        let Some(ref repo_id) = model.hf_repo_id else {
587            return Ok(UpdateCheckResult {
588                model_id,
589                update_available: false,
590                details: None,
591            });
592        };
593
594        let Some(ref quantization) = model.quantization else {
595            return Ok(UpdateCheckResult {
596                model_id,
597                update_available: false,
598                details: None,
599            });
600        };
601
602        // Get local file metadata
603        let local_files = self
604            .model_files_repo
605            .get_by_model_id(model_id)
606            .await
607            .map_err(|e| RepositoryError::Storage(e.to_string()))?;
608
609        if local_files.is_empty() {
610            return Ok(UpdateCheckResult {
611                model_id,
612                update_available: false,
613                details: None,
614            });
615        }
616
617        // Get remote file metadata from HuggingFace
618        let remote_files = self
619            .hf_client
620            .get_quantization_files(repo_id, quantization)
621            .await
622            .map_err(|e| RepositoryError::Storage(format!("Failed to fetch remote files: {e}")))?;
623
624        // Compare OIDs
625        let mut changes = Vec::new();
626
627        for local_file in &local_files {
628            let Some(ref local_oid) = local_file.hf_oid else {
629                continue;
630            };
631
632            // Find matching remote file by path
633            if let Some(remote_file) = remote_files.iter().find(|f| f.path == local_file.file_path)
634                && let Some(ref remote_oid) = remote_file.oid
635                && local_oid != remote_oid
636            {
637                let old_oid_str: String = local_oid.clone();
638                let new_oid_str: String = remote_oid.clone();
639                #[allow(clippy::cast_sign_loss)]
640                let index = local_file.file_index as usize;
641                changes.push(ShardUpdate {
642                    index,
643                    file_path: local_file.file_path.clone(),
644                    old_oid: old_oid_str,
645                    new_oid: new_oid_str,
646                });
647            }
648        }
649
650        let update_available = !changes.is_empty();
651        let details = if update_available {
652            Some(UpdateDetails {
653                changed_shards: changes.len(),
654                changes,
655            })
656        } else {
657            None
658        };
659
660        Ok(UpdateCheckResult {
661            model_id,
662            update_available,
663            details,
664        })
665    }
666
667    /// Repair a model by re-downloading corrupt or missing shards.
668    ///
669    /// # Arguments
670    ///
671    /// * `model_id` - ID of the model to repair
672    /// * `shard_indices` - Optional list of specific shard indices to repair.
673    ///   If `None`, all unhealthy shards will be repaired.
674    pub async fn repair_model(
675        &self,
676        model_id: i64,
677        shard_indices: Option<Vec<usize>>,
678    ) -> Result<String, String> {
679        // Acquire downloading lock
680        let _guard = self
681            .operation_lock
682            .try_acquire(model_id, OperationType::Downloading)
683            .await?;
684
685        // Get model metadata
686        let model = self
687            .model_repo
688            .get_by_id(model_id)
689            .await
690            .map_err(|e| format!("Failed to get model: {e}"))?;
691
692        let Some(ref repo_id) = model.hf_repo_id else {
693            return Err("Model does not have HuggingFace repository information".to_string());
694        };
695
696        let Some(ref quantization) = model.quantization else {
697            return Err("Model does not have quantization information".to_string());
698        };
699
700        // Get file metadata
701        let model_files = self
702            .model_files_repo
703            .get_by_model_id(model_id)
704            .await
705            .map_err(|e| format!("Failed to get model files: {e}"))?;
706
707        // Get base directory from model's file path
708        let base_dir = model
709            .file_path
710            .parent()
711            .ok_or_else(|| "Failed to get model directory".to_string())?
712            .to_path_buf();
713
714        // Determine which shards to repair
715        let shards_to_repair: Vec<&ModelFile> = if let Some(indices) = shard_indices {
716            #[allow(clippy::cast_sign_loss)]
717            let filter_fn = |f: &&ModelFile| indices.contains(&(f.file_index as usize));
718            model_files.iter().filter(filter_fn).collect()
719        } else {
720            // Verify all shards to find unhealthy ones
721            let mut unhealthy = Vec::new();
722            for file in &model_files {
723                let (tx, _rx) = mpsc::channel(1);
724                let resolved_path = base_dir.join(&file.file_path);
725                let health = Self::verify_shard(file, &resolved_path, model_id, 0, 1, &tx).await;
726                match health {
727                    ShardHealth::Corrupt { .. } | ShardHealth::Missing => {
728                        unhealthy.push(file);
729                    }
730                    _ => {}
731                }
732            }
733            unhealthy
734        };
735
736        if shards_to_repair.is_empty() {
737            return Err("No unhealthy shards found to repair".to_string());
738        }
739
740        // Delete corrupt/missing files
741        for file in &shards_to_repair {
742            let resolved_path = base_dir.join(&file.file_path);
743            if resolved_path.exists()
744                && let Err(e) = tokio::fs::remove_file(&resolved_path).await
745            {
746                tracing::warn!(
747                    model_id = model_id,
748                    file_path = %file.file_path,
749                    error = %e,
750                    "Failed to delete corrupt file"
751                );
752            }
753        }
754
755        // Trigger re-download
756        let download_id = self
757            .download_trigger
758            .queue_download(repo_id.clone(), Some(quantization.clone()))
759            .await
760            .map_err(|e| format!("Failed to queue download: {e}"))?;
761
762        Ok(download_id)
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769
770    #[tokio::test]
771    async fn test_operation_lock_single_acquire() {
772        let lock = ModelOperationLock::new();
773        let guard = lock.try_acquire(1, OperationType::Verifying).await;
774        assert!(guard.is_ok());
775    }
776
777    #[tokio::test]
778    async fn test_operation_lock_double_acquire_fails() {
779        let lock = ModelOperationLock::new();
780        let _guard1 = lock.try_acquire(1, OperationType::Verifying).await.unwrap();
781        let guard2 = lock.try_acquire(1, OperationType::Downloading).await;
782        assert!(guard2.is_err());
783    }
784
785    #[tokio::test]
786    async fn test_operation_lock_release_on_drop() {
787        let lock = ModelOperationLock::new();
788        {
789            let _guard = lock.try_acquire(1, OperationType::Verifying).await.unwrap();
790        }
791        // Give the drop task time to complete
792        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
793
794        let guard2 = lock.try_acquire(1, OperationType::Downloading).await;
795        assert!(guard2.is_ok());
796    }
797
798    #[tokio::test]
799    async fn test_operation_lock_different_models() {
800        let lock = ModelOperationLock::new();
801        let guard1 = lock.try_acquire(1, OperationType::Verifying).await;
802        let guard2 = lock.try_acquire(2, OperationType::Verifying).await;
803        assert!(guard1.is_ok());
804        assert!(guard2.is_ok());
805    }
806}