Skip to content

Commit 6df1799

Browse files
committed
sdk: Atomic writes in LocalFileIdentifiableStore
Previously, `add()` and `commit()` opened the target file with mode `"w"`, which truncates it immediately on open. A crash or exception mid-write would leave an empty or partial JSON file, with no way to recover the original content. A new `_write_atomic()` helper writes to a temp file in the same directory, then uses `os.replace()` (atomic rename) to swap it into place. Both `add()` and `commit()` now use this helper.
1 parent 6b90282 commit 6df1799

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

sdk/basyx/aas/backend/local_file.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import json
1717
import os
1818
import hashlib
19+
import tempfile
1920
import threading
2021
import warnings
2122
import weakref
@@ -94,6 +95,25 @@ def get_item(self, identifier: model.Identifier) -> model.Identifiable:
9495
except KeyError as e:
9596
raise KeyError("No Identifiable with id {} found in local file database".format(identifier)) from e
9697

98+
def _write_atomic(self, x: model.Identifiable) -> None:
99+
"""
100+
Serialize x to a temp file in the store directory, then atomically replace the final file.
101+
102+
Using os.replace() (rename(2) on POSIX) ensures readers always see a complete file — never
103+
a partially-written one from a crash or concurrent access mid-write.
104+
"""
105+
final_path = "{}/{}.json".format(self.directory_path, self._transform_id(x.id))
106+
tmp_fd, tmp_path = tempfile.mkstemp(dir=self.directory_path, suffix=".tmp")
107+
try:
108+
with os.fdopen(tmp_fd, "w") as tmp_file:
109+
json.dump({"data": x}, tmp_file, cls=json_serialization.AASToJsonEncoder, indent=4)
110+
os.replace(tmp_path, final_path)
111+
# Catch all `Exception`s, as well as `KeyboardInterrupt` and `SystemExit` too, so the temp
112+
# file is never left behind even if the process is being torn down:
113+
except BaseException:
114+
os.unlink(tmp_path)
115+
raise
116+
97117
def add(self, x: model.Identifiable) -> None:
98118
"""
99119
Add an object to the store
@@ -103,10 +123,9 @@ def add(self, x: model.Identifiable) -> None:
103123
logger.debug("Adding object %s to Local File Store ...", repr(x))
104124
if os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
105125
raise KeyError("Identifiable with id {} already exists in local file database".format(x.id))
106-
with open("{}/{}.json".format(self.directory_path, self._transform_id(x.id)), "w") as file:
107-
json.dump({"data": x}, file, cls=json_serialization.AASToJsonEncoder, indent=4)
108-
with self._object_cache_lock:
109-
self._object_cache[x.id] = x
126+
self._write_atomic(x)
127+
with self._object_cache_lock:
128+
self._object_cache[x.id] = x
110129

111130
def commit(self, x: model.Identifiable) -> None:
112131
"""
@@ -117,8 +136,7 @@ def commit(self, x: model.Identifiable) -> None:
117136
"""
118137
if not os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
119138
raise KeyError("No AAS object with id {} exists in local file database".format(x.id))
120-
with open("{}/{}.json".format(self.directory_path, self._transform_id(x.id)), "w") as file:
121-
json.dump({"data": x}, file, cls=json_serialization.AASToJsonEncoder, indent=4)
139+
self._write_atomic(x)
122140

123141
def discard(self, x: model.Identifiable) -> None:
124142
"""

0 commit comments

Comments
 (0)