diff options
author | Frederick Yin <fkfd@fkfd.me> | 2021-10-19 21:05:01 +0800 |
---|---|---|
committer | Frederick Yin <fkfd@fkfd.me> | 2021-10-19 21:05:01 +0800 |
commit | 1dfa5659e322136000ac37324e52edaaef5b28ab (patch) | |
tree | 17babd40e56d32cda95e7fa5b6aab2a05fa94259 | |
parent | 4af47111b83b3aa2ce30dd3075668f98a1030603 (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.py | 45 |
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: |