Skip to content

Commit dbda813

Browse files
ljeub-pometryfabianmurariuricopinazo
authored
Mutation lock for server (#2664)
* add explicit lock for workdir * graphs in the cache need to be unlocked * remove dbg * shouldn't clone the folder as it keeps the lock longer than it is supposed to --------- Co-authored-by: Fabian Murariu <2404621+fabianmurariu@users.noreply.github.com> Co-authored-by: Pedro Rico Pinazo <ricopinazo@gmail.com>
1 parent e167eae commit dbda813

14 files changed

Lines changed: 410 additions & 181 deletions

File tree

raphtory-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod python;
66

77
pub mod inherit;
88
pub mod iter;
9+
pub mod to_millis;
910

1011
use serde::{Deserialize, Serialize};
1112

raphtory-api/src/to_millis.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
2+
3+
pub trait ToMillis {
4+
fn to_millis(&self) -> Result<i64, SystemTimeError>;
5+
}
6+
impl ToMillis for SystemTime {
7+
fn to_millis(&self) -> Result<i64, SystemTimeError> {
8+
Ok(self.duration_since(UNIX_EPOCH)?.as_millis() as i64)
9+
}
10+
}

raphtory-graphql/src/data.rs

Lines changed: 134 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ use crate::{
88
blocking_io,
99
graph::{
1010
filtering::{GraphAccessFilter, GraphRowFilter, HiddenKeys},
11+
meta_graph::MetaGraph,
1112
namespace::Namespace,
1213
namespaced_item::NamespacedItem,
1314
vectorised_graph::GqlVectorisedGraph,
1415
},
1516
},
1617
paths::{
1718
mark_dirty, ExistingGraphFolder, InternalPathValidationError, PathValidationError,
18-
ValidGraphPaths, ValidWriteableGraphFolder,
19+
UnlockedGraphFolder, ValidGraphPaths, ValidWriteableGraphFolder,
1920
},
2021
rayon::blocking_compute,
2122
GQLError,
@@ -39,12 +40,14 @@ use raphtory::{
3940
},
4041
};
4142
use std::{
43+
cmp::Ordering,
4244
fs, io,
4345
io::{Read, Seek},
4446
ops::Deref,
4547
path::{Path, PathBuf},
4648
sync::Arc,
4749
};
50+
use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock, RwLockReadGuard};
4851
use tracing::{error, warn};
4952
use walkdir::WalkDir;
5053

@@ -145,13 +148,107 @@ pub(crate) fn get_relative_path(
145148

146149
/// Inner struct with a drop implementation that cleans up the graphs
147150
pub struct DataInner {
148-
pub(crate) work_dir: PathBuf,
151+
work_dir: Arc<RwLock<PathBuf>>,
149152
pub(crate) cache: GraphCache,
150153
pub(crate) vector_cache: LazyDiskVectorCache,
151154
pub(crate) graph_conf: Config,
152155
pub(crate) auth_policy: Option<Arc<dyn AuthorizationPolicy>>,
153156
}
154157

158+
#[derive(Debug, Clone)]
159+
pub struct WorkDirWriteGuard {
160+
guard: Arc<OwnedRwLockWriteGuard<PathBuf>>,
161+
}
162+
163+
impl WorkDirWriteGuard {
164+
pub fn path(&self) -> &Path {
165+
&self.guard
166+
}
167+
168+
pub fn to_path_buf(&self) -> PathBuf {
169+
self.path().to_path_buf()
170+
}
171+
172+
pub fn validate_path_for_insert(
173+
self,
174+
path: &str,
175+
overwrite: bool,
176+
) -> Result<ValidWriteableGraphFolder, PathValidationError> {
177+
if overwrite {
178+
ValidWriteableGraphFolder::try_existing_or_new(self, path)
179+
} else {
180+
ValidWriteableGraphFolder::try_new(self, path)
181+
}
182+
}
183+
}
184+
185+
impl PartialEq for WorkDirWriteGuard {
186+
fn eq(&self, other: &Self) -> bool {
187+
self.path() == other.path()
188+
}
189+
}
190+
191+
impl Eq for WorkDirWriteGuard {}
192+
193+
impl PartialOrd for WorkDirWriteGuard {
194+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195+
self.path().partial_cmp(other.path())
196+
}
197+
}
198+
199+
impl Ord for WorkDirWriteGuard {
200+
fn cmp(&self, other: &Self) -> Ordering {
201+
self.path().cmp(other.path())
202+
}
203+
}
204+
205+
#[derive(Debug, Clone)]
206+
pub enum WorkDirGuard {
207+
Read {
208+
guard: Arc<OwnedRwLockReadGuard<PathBuf>>,
209+
},
210+
Write(WorkDirWriteGuard),
211+
}
212+
213+
impl From<WorkDirWriteGuard> for WorkDirGuard {
214+
fn from(value: WorkDirWriteGuard) -> Self {
215+
Self::Write(value)
216+
}
217+
}
218+
219+
impl PartialEq for WorkDirGuard {
220+
fn eq(&self, other: &Self) -> bool {
221+
self.path() == other.path()
222+
}
223+
}
224+
225+
impl Eq for WorkDirGuard {}
226+
227+
impl PartialOrd for WorkDirGuard {
228+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
229+
self.path().partial_cmp(other.path())
230+
}
231+
}
232+
233+
impl Ord for WorkDirGuard {
234+
fn cmp(&self, other: &Self) -> Ordering {
235+
self.path().cmp(other.path())
236+
}
237+
}
238+
239+
impl WorkDirGuard {
240+
pub fn path(&self) -> &Path {
241+
match self {
242+
WorkDirGuard::Read { guard } => &guard,
243+
WorkDirGuard::Write(guard) => guard.path(),
244+
}
245+
}
246+
247+
pub fn to_path_buf(&self) -> PathBuf {
248+
self.path().to_path_buf()
249+
}
250+
}
251+
155252
/// Outer data struct that wraps the inner data to make sure it is only dropped once
156253
#[derive(Clone)]
157254
pub struct Data {
@@ -198,7 +295,7 @@ impl Data {
198295

199296
Self {
200297
inner: Arc::new(DataInner {
201-
work_dir: work_dir.to_path_buf(),
298+
work_dir: Arc::new(RwLock::new(work_dir.to_path_buf())),
202299
cache,
203300
vector_cache: LazyDiskVectorCache::new(work_dir.join(".vector-cache")),
204301
graph_conf,
@@ -208,28 +305,27 @@ impl Data {
208305
}
209306
}
210307

308+
pub async fn work_dir_read(&self) -> WorkDirGuard {
309+
let guard = Arc::new(self.work_dir.clone().read_owned().await);
310+
WorkDirGuard::Read { guard }
311+
}
312+
313+
pub async fn work_dir_write(&self) -> WorkDirWriteGuard {
314+
let guard = Arc::new(self.work_dir.clone().write_owned().await);
315+
WorkDirWriteGuard { guard }
316+
}
317+
211318
pub(crate) fn set_auth_policy(&mut self, policy: Arc<dyn AuthorizationPolicy>) {
212319
Arc::get_mut(&mut self.inner)
213320
.expect("Data is not uniquely owned when setting auth_policy")
214321
.auth_policy = Some(policy);
215322
}
216323

217-
pub fn validate_path_for_insert(
218-
&self,
219-
path: &str,
220-
overwrite: bool,
221-
) -> Result<ValidWriteableGraphFolder, PathValidationError> {
222-
if overwrite {
223-
ValidWriteableGraphFolder::try_existing_or_new(self.work_dir.clone(), path)
224-
} else {
225-
ValidWriteableGraphFolder::try_new(self.work_dir.clone(), path)
226-
}
227-
}
228-
229324
/// Validates that `ns_path` exists and is a namespace, returning the `Namespace`
230325
/// so callers can enumerate descendants via `get_all_children()`.
231-
pub fn get_namespace(&self, ns_path: &str) -> Result<Namespace, PathValidationError> {
232-
Namespace::try_new(self.work_dir.clone(), ns_path.to_string())
326+
pub async fn get_namespace(&self, ns_path: &str) -> Result<Namespace, PathValidationError> {
327+
let work_dir = self.work_dir_read().await;
328+
Namespace::try_new(work_dir, ns_path.to_string())
233329
}
234330

235331
/// # ⚠ Bypasses all permission checks — do not call from resolvers directly.
@@ -318,7 +414,8 @@ impl Data {
318414
}
319415

320416
pub async fn delete_graph(&self, path: &str) -> Result<(), DeletionError> {
321-
let graph_folder = ExistingGraphFolder::try_from(self.work_dir.clone(), path)?;
417+
let work_dir = self.work_dir_write().await;
418+
let graph_folder = ExistingGraphFolder::try_from(work_dir.into(), path)?;
322419
self.delete_graph_inner(graph_folder)
323420
.await
324421
.map_err(|err| DeletionError::from_inner(path, err))?;
@@ -327,15 +424,15 @@ impl Data {
327424

328425
pub async fn delete_namespace(
329426
&self,
330-
path: &str,
427+
namespace: Namespace,
331428
descendants: &Vec<NamespacedItem>,
332429
) -> Result<(), DeletionError> {
430+
let path = namespace.local_path();
333431
if path.is_empty() {
334432
return Err(DeletionError::PathValidation(
335433
PathValidationError::EmptyPath,
336434
));
337435
}
338-
let namespace = Namespace::try_new(self.work_dir.clone(), path.to_string())?;
339436
let root = namespace.current_dir().to_path_buf();
340437
let dirty_file = mark_dirty(&root).map_err(|err| {
341438
DeletionError::from_inner(path, MutationErrorInner::InvalidInternal(err))
@@ -360,7 +457,9 @@ impl Data {
360457
}
361458

362459
pub async fn create_namespace(&self, path: &str) -> Result<(), InsertionError> {
363-
let target = crate::paths::validate_path_for_namespace_create(self.work_dir.clone(), path)?;
460+
let work_dir = self.work_dir_write().await;
461+
let target =
462+
crate::paths::validate_path_for_namespace_create(work_dir.to_path_buf(), path)?;
364463
let mut cleanup_root = target.as_path();
365464
while let Some(parent) = cleanup_root.parent() {
366465
if parent.is_dir() {
@@ -435,15 +534,15 @@ impl Data {
435534
Ok(())
436535
}
437536

438-
pub fn get_all_graph_folders(&self) -> impl Iterator<Item = ExistingGraphFolder> {
439-
let base_path = self.work_dir.clone();
440-
WalkDir::new(&self.work_dir)
537+
pub async fn get_all_graph_folders(&self) -> impl Iterator<Item = ExistingGraphFolder> {
538+
let work_dir = self.work_dir_read().await;
539+
WalkDir::new(work_dir.path())
441540
.into_iter()
442541
.filter_map(move |e| {
443542
let entry = e.ok()?;
444543
let path = entry.path();
445-
let relative = get_relative_path(&base_path, path).ok()?;
446-
let folder = ExistingGraphFolder::try_from(base_path.clone(), &relative).ok()?;
544+
let relative = get_relative_path(work_dir.path(), path).ok()?;
545+
let folder = ExistingGraphFolder::try_from(work_dir.clone(), &relative).ok()?;
447546
Some(folder)
448547
})
449548
}
@@ -459,7 +558,8 @@ impl Data {
459558
}
460559

461560
async fn read_graph_from_disk(&self, path: &str) -> Result<GraphWithVectors, GQLError> {
462-
let folder = ExistingGraphFolder::try_from(self.work_dir.clone(), path)?;
561+
let work_dir = self.work_dir_read().await;
562+
let folder = ExistingGraphFolder::try_from(work_dir, path)?;
463563
Ok(self.read_graph_from_disk_inner(folder).await?)
464564
}
465565
}
@@ -653,7 +753,7 @@ impl Data {
653753
path: &str,
654754
perm: GraphPermission,
655755
graph_type: Option<GqlGraphType>,
656-
) -> async_graphql::Result<(ExistingGraphFolder, DynamicGraph)> {
756+
) -> async_graphql::Result<(UnlockedGraphFolder, DynamicGraph)> {
657757
let gwv = self.get_graph(path).await?;
658758
let typed_graph = match graph_type {
659759
Some(GqlGraphType::Event) => match gwv.graph() {
@@ -691,7 +791,7 @@ impl Data {
691791
ctx: &Context<'_>,
692792
path: &str,
693793
graph_type: Option<GqlGraphType>,
694-
) -> async_graphql::Result<Option<(ExistingGraphFolder, DynamicGraph)>> {
794+
) -> async_graphql::Result<Option<(UnlockedGraphFolder, DynamicGraph)>> {
695795
match require_at_least_read(ctx, &self.auth_policy, path) {
696796
Ok(perm) => self.load_and_filter(path, perm, graph_type).await.map(Some),
697797
Err(_) => Ok(None),
@@ -707,7 +807,7 @@ impl Data {
707807
ctx: &Context<'_>,
708808
path: &str,
709809
graph_type: Option<GqlGraphType>,
710-
) -> async_graphql::Result<(ExistingGraphFolder, DynamicGraph)> {
810+
) -> async_graphql::Result<(UnlockedGraphFolder, DynamicGraph)> {
711811
let perm = require_at_least_read(ctx, &self.auth_policy, path)?;
712812
self.load_and_filter(path, perm, graph_type).await
713813
}
@@ -789,8 +889,9 @@ pub(crate) mod data_tests {
789889
data: &Data,
790890
graphs: &HashMap<String, MaterializedGraph>,
791891
) -> Result<(), InsertionError> {
892+
let work_dir = data.work_dir_write().await;
792893
for (name, graph) in graphs.into_iter() {
793-
let folder = data.validate_path_for_insert(name, true)?;
894+
let folder = work_dir.clone().validate_path_for_insert(name, true)?;
794895
data.insert_graph(folder, graph.clone()).await?;
795896
}
796897
Ok(())
@@ -888,8 +989,9 @@ pub(crate) mod data_tests {
888989

889990
let paths = data
890991
.get_all_graph_folders()
992+
.await
891993
.into_iter()
892-
.map(|folder| folder.0.root().to_path_buf())
994+
.map(|folder| folder.folder.root().to_path_buf())
893995
.collect_vec();
894996

895997
assert_eq!(paths.len(), 5);

raphtory-graphql/src/graph.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
paths::{ExistingGraphFolder, ValidGraphPaths},
2+
paths::{ExistingGraphFolder, UnlockedGraphFolder, ValidGraphPaths},
33
rayon::blocking_compute,
44
};
55
#[cfg(feature = "search")]
@@ -43,7 +43,7 @@ pub struct GraphWithVectors {
4343
pub struct GraphWithVectorsInner {
4444
pub graph: MaterializedGraph,
4545
pub vectors: Option<VectorisedGraph<MaterializedGraph>>,
46-
pub folder: ExistingGraphFolder,
46+
pub folder: UnlockedGraphFolder,
4747
pub is_dirty: AtomicBool,
4848
pub is_flushing: AtomicBool,
4949
}
@@ -57,7 +57,7 @@ impl GraphWithVectors {
5757
let inner = Arc::new(GraphWithVectorsInner {
5858
graph,
5959
vectors,
60-
folder,
60+
folder: folder.unlock(),
6161
is_dirty: AtomicBool::new(false),
6262
is_flushing: AtomicBool::new(false),
6363
});
@@ -93,7 +93,7 @@ impl GraphWithVectors {
9393
self.inner.vectors.as_ref()
9494
}
9595

96-
pub fn folder(&self) -> &ExistingGraphFolder {
96+
pub fn folder(&self) -> &UnlockedGraphFolder {
9797
&self.inner.folder
9898
}
9999
pub fn set_dirty(&self, is_dirty: bool) {

raphtory-graphql/src/model/graph/graph.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
plugins::graph_algorithm_plugin::GraphAlgorithmPlugin,
1919
schema::graph_schema::GraphSchema,
2020
},
21-
paths::{ExistingGraphFolder, PathValidationError, ValidGraphPaths},
21+
paths::{ExistingGraphFolder, PathValidationError, UnlockedGraphFolder, ValidGraphPaths},
2222
rayon::blocking_compute,
2323
};
2424
use async_graphql::Context;
@@ -60,7 +60,7 @@ use std::{
6060
#[derive(ResolvedObject, Clone)]
6161
#[graphql(name = "Graph")]
6262
pub(crate) struct GqlGraph {
63-
path: ExistingGraphFolder,
63+
path: UnlockedGraphFolder,
6464
graph: DynamicGraph,
6565
}
6666

@@ -71,7 +71,7 @@ impl From<GraphWithVectors> for GqlGraph {
7171
}
7272

7373
impl GqlGraph {
74-
pub fn new<G: StaticGraphViewOps + IntoDynamic>(path: ExistingGraphFolder, graph: G) -> Self {
74+
pub fn new<G: StaticGraphViewOps + IntoDynamic>(path: UnlockedGraphFolder, graph: G) -> Self {
7575
Self {
7676
path,
7777
graph: graph.into_dynamic(),

0 commit comments

Comments
 (0)