summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederick Yin <fkfd@fkfd.me>2021-10-19 21:05:01 +0800
committerFrederick Yin <fkfd@fkfd.me>2021-10-19 21:05:01 +0800
commit1dfa5659e322136000ac37324e52edaaef5b28ab (patch)
tree17babd40e56d32cda95e7fa5b6aab2a05fa94259
parent4af47111b83b3aa2ce30dd3075668f98a1030603 (diff)
Database lock
Is a lockfile named "$DATABASE_NAME.lock". `Database._write()` prevents race conditions by enforcing the lock, and waiting until a previous lock is released.
-rw-r--r--jimbrella/database.py45
1 files changed, 43 insertions, 2 deletions
diff --git a/jimbrella/database.py b/jimbrella/database.py
index e841650..3d835a8 100644
--- a/jimbrella/database.py
+++ b/jimbrella/database.py
@@ -1,4 +1,5 @@
import csv
+import os
from datetime import datetime
@@ -9,7 +10,7 @@ class Database:
Currently, the data are represented in a csv file.
Explanation of data types:
- - id | a unique numerical identity of unsigned integer type.
+ - id | a unique numerical identity of unsigned integer type.
- uint | whatever fits the regex /[0-9]+/. despite carrying a numerical value, it is
| internally represented as a string.
- string | any UTF-8 string.
@@ -37,6 +38,33 @@ class Database:
"""
self.path = path
+ def _lock(self) -> int:
+ """Create a lockfile for database.
+
+ If the database is called "db.csv", the lockfile, located in the same
+ directory, would be called "db.csv.lock".
+
+ Returns 1 if the lockfile is successfully created, 0 if the lockfile
+ already exists (strong hint of unrecovered crash or potential race
+ condition - caller of this method should abort whatever it is doing)
+ """
+ try:
+ f = open(self.path + ".lock", "x")
+ f.close()
+ return 1
+ except FileExistsError:
+ return 0
+
+ def _unlock(self) -> None:
+ """Remove lockfile created by _lock.
+
+ If there is no lockfile, simply ignore.
+ """
+ try:
+ os.remove(self.path + ".lock")
+ except FileNotFoundError:
+ pass
+
def _read(self) -> list:
"""Deserialize database."""
# Create database if it does not yet exist
@@ -67,10 +95,15 @@ class Database:
def _write(self, umbrellas: list) -> None:
"""Serialize database. When a failure occurs, abort and recover data."""
+ # make backup in memory
with open(self.path) as f:
backup = f.read()
f.close()
+ # wait until database is locked for this write
+ while not self._lock():
+ continue
+
f = open(self.path, "w")
try:
writer = csv.writer(f)
@@ -84,7 +117,9 @@ class Database:
umb["tenant_id"],
umb["tenant_phone"],
umb["tenant_email"],
- umb["lent_at"].isoformat(timespec="seconds") if umb["lent_at"] else "",
+ umb["lent_at"].isoformat(timespec="seconds")
+ if umb["lent_at"]
+ else "",
]
for umb in umbrellas
]
@@ -98,6 +133,8 @@ class Database:
f.write(backup)
f.close()
+ self._unlock()
+
def _update(self, update: dict) -> list:
"""Update status of one umbrella, and return the entire updated database."""
umbrellas = self._read()
@@ -116,6 +153,10 @@ class Database:
if umb["serial"] == serial:
return umb
+ def read(self) -> list:
+ """Currently an alias for `_read()`."""
+ return self._read()
+
def take_away(
self, serial, tenant_name, tenant_id, tenant_phone="", tenant_email=""
) -> None: