Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Implement storage lock for off-chain worker #6004

@jimmychu0807

Description

@jimmychu0807

@tomusdrw outlined an implementation of storage lock for off-chain worker here. Copying the content here so it is not lost.

const MAX_SPIN_ITERATIONS: usize = 1_000; // we wait up to 100 seconds
const SLEEP_TIME_MS: usize = 100;

pub struct StorageLock<'a, BlockNumber = u64> {
    storage: StorageValue<'a>,
    blocks: Option<BlockNumber>,
}

impl<'a> StorageLock<'a> {
    pub fn new(id: &'a str) -> Self {
        StorageLock {
            storage: StorageValue::persistent(id),
            blocks: None,
        }
    }
}

impl<'a, BlockNumber: Ord> StorageLock<'a, BlockNumber> {
    pub fn with_block_limit(id :&'a str, blocks: BlockNumber) -> Self {
        StorageLock {
            storage: StorageValue::persistent(id),
            blocks,
        }
    }
    
    pub fn try_lock<'b>(&'b mut self) -> Result<
        StorageLockGuard<'a, 'b, BlockNumber>,
        ()
    > {
        let current_block_number = frame_system::Module<T>::block_number();
        let release_at = current_block_number + self.blocks.clone().unwrap_or(0.into());
        
        let res: Result<Result<BlockNumber, BlockNumber>, ()> = self.storage
            .mutate(|s: Option<Option<BlockNumber>>| {
    			match s {
    			    // no lock set, we can safely acquire it
    				None => Ok(release_at),
    				// lock is set, but it's old. We can re-acquire it.
    				Some(Some(number)) if number < current_block_number => Ok(release_at),
    				// lock is present and is still active, fail.
    				_ => Err(()),
    			}
    		});
    		
    	if let Ok(Ok(_)) = res {
    	    Ok(StorageLockGuard {
    	        lock: Some(self),
    	    })
    	} else {
    	    Err(())
    	}
    }
    
    pub fn spin_lock<'b>(&'b mut self) -> Result<
        StorageLockGuard<'a, 'b, BlockNumber>,
        ()
    > {
        for i in 1..MAX_SPIN_ITERATIONS {
            if let Ok(guard) = self.try_lock() {
                return Ok(guard);
            }
            sp_io::offchain::sleep(SLEEP_TIME_MS);
        }
        
        Err(())
    }
    
    fn unlock(&mut self) {
        let block: BlockNumber = 0.into();
        self.storage.set(&block);
    }
}

pub struct StorageLockGuard<'a, 'b, BlockNumber> {
    lock: Option<&'b mut StorageLock<'a, BlockNumber>>,
}

impl<'a, 'b, BlockNumber> StorageLockGuard<'a, 'b, BlockNumber> {
    pub fn forget(self) {
        self.lock.take()
    }
}

impl<'a, 'b, BlockNumber> Drop for StorageLockGuard<'a, 'b, BlockNumber> {
    fn drop(&mut self) {
        if Some(lock) = self.lock.take() {
            lock.unlock();
        }
    }
}

This more complex one allows you to set some time limit (we could do it in seconds instead of block numbers though). For how long the block is valid.
This has some useful properties, for instance:

  • If your node get's killed in the middle of fetching GithubInfo, it will never run the offchain worker any more, cause the storage entry will be stuck at the locked state.
  • It allows you to have a grace period, where you send a transaction and then want to prevent other offchain workers from running, hoping the transaction will get accepted in the meantime (use case for guard.forget()). We do have a logic like this in offchain phragmen and im-online already.

fyi @drahnr

Let's implement this in Substrate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions