Skip to main content

soil_client/db/
offchain.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! RocksDB-based offchain workers local storage.
8
9use std::{collections::HashMap, sync::Arc};
10
11use super::{columns, Database, DbHash, Transaction};
12use log::error;
13use parking_lot::Mutex;
14
15/// Offchain local storage
16#[derive(Clone)]
17pub struct LocalStorage {
18	db: Arc<dyn Database<DbHash>>,
19	locks: Arc<Mutex<HashMap<Vec<u8>, Arc<Mutex<()>>>>>,
20}
21
22impl std::fmt::Debug for LocalStorage {
23	fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
24		fmt.debug_struct("LocalStorage").finish()
25	}
26}
27
28impl LocalStorage {
29	/// Create new offchain storage for tests (backed by memorydb)
30	#[cfg(any(feature = "test-helpers", test))]
31	pub fn new_test() -> Self {
32		let db = kvdb_memorydb::create(super::utils::NUM_COLUMNS);
33		let db = subsoil::database::as_database(db);
34		Self::new(db as _)
35	}
36
37	/// Create offchain local storage with given `KeyValueDB` backend.
38	pub fn new(db: Arc<dyn Database<DbHash>>) -> Self {
39		Self { db, locks: Default::default() }
40	}
41}
42
43impl subsoil::core::offchain::OffchainStorage for LocalStorage {
44	fn set(&mut self, prefix: &[u8], key: &[u8], value: &[u8]) {
45		let mut tx = Transaction::new();
46		tx.set(columns::OFFCHAIN, &concatenate_prefix_and_key(prefix, key), value);
47
48		if let Err(err) = self.db.commit(tx) {
49			error!("Error setting on local storage: {}", err)
50		}
51	}
52
53	fn remove(&mut self, prefix: &[u8], key: &[u8]) {
54		let mut tx = Transaction::new();
55		tx.remove(columns::OFFCHAIN, &concatenate_prefix_and_key(prefix, key));
56
57		if let Err(err) = self.db.commit(tx) {
58			error!("Error removing on local storage: {}", err)
59		}
60	}
61
62	fn get(&self, prefix: &[u8], key: &[u8]) -> Option<Vec<u8>> {
63		self.db.get(columns::OFFCHAIN, &concatenate_prefix_and_key(prefix, key))
64	}
65
66	fn compare_and_set(
67		&mut self,
68		prefix: &[u8],
69		item_key: &[u8],
70		old_value: Option<&[u8]>,
71		new_value: &[u8],
72	) -> bool {
73		let key = concatenate_prefix_and_key(prefix, item_key);
74		let key_lock = {
75			let mut locks = self.locks.lock();
76			locks.entry(key.clone()).or_default().clone()
77		};
78
79		let is_set;
80		{
81			let _key_guard = key_lock.lock();
82			let val = self.db.get(columns::OFFCHAIN, &key);
83			is_set = val.as_deref() == old_value;
84
85			if is_set {
86				self.set(prefix, item_key, new_value)
87			}
88		}
89
90		// clean the lock map if we're the only entry
91		let mut locks = self.locks.lock();
92		{
93			drop(key_lock);
94			let key_lock = locks.get_mut(&key);
95			if key_lock.and_then(Arc::get_mut).is_some() {
96				locks.remove(&key);
97			}
98		}
99		is_set
100	}
101}
102
103/// Concatenate the prefix and key to create an offchain key in the db.
104pub(crate) fn concatenate_prefix_and_key(prefix: &[u8], key: &[u8]) -> Vec<u8> {
105	prefix.iter().chain(key.iter()).cloned().collect()
106}
107
108#[cfg(test)]
109mod tests {
110	use super::*;
111	use subsoil::core::offchain::OffchainStorage;
112
113	#[test]
114	fn should_compare_and_set_and_clear_the_locks_map() {
115		let mut storage = LocalStorage::new_test();
116		let prefix = b"prefix";
117		let key = b"key";
118		let value = b"value";
119
120		storage.set(prefix, key, value);
121		assert_eq!(storage.get(prefix, key), Some(value.to_vec()));
122
123		assert_eq!(storage.compare_and_set(prefix, key, Some(value), b"asd"), true);
124		assert_eq!(storage.get(prefix, key), Some(b"asd".to_vec()));
125		assert!(storage.locks.lock().is_empty(), "Locks map should be empty!");
126	}
127
128	#[test]
129	fn should_compare_and_set_on_empty_field() {
130		let mut storage = LocalStorage::new_test();
131		let prefix = b"prefix";
132		let key = b"key";
133
134		assert_eq!(storage.compare_and_set(prefix, key, None, b"asd"), true);
135		assert_eq!(storage.get(prefix, key), Some(b"asd".to_vec()));
136		assert!(storage.locks.lock().is_empty(), "Locks map should be empty!");
137	}
138}