/
opt_locking.py
155 lines (133 loc) · 6.12 KB
/
opt_locking.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import datetime
from decimal import Decimal
from logic_bank.exec_row_logic.logic_row import LogicRow
from logic_bank.extensions.rule_extensions import RuleExtension
from logic_bank.logic_bank import Rule
from database import models
import logging
import sqlalchemy
from sqlalchemy import inspect
from sqlalchemy import event
from safrs import SAFRSBase
from safrs.util import classproperty
from safrs.errors import JsonapiError
from http import HTTPStatus
from config.config import OptLocking
from config.config import Args as args
logger = logging.getLogger(__name__)
def opt_locking_setup(session):
"""
Listen_for read events - set rows' CheckSum property for optimistic locking
Called at Server start (api_logic_server_run)...
"""
@event.listens_for(session, 'loaded_as_persistent')
def receive_loaded_as_persistent(session, instance):
"listen for the 'loaded_as_persistent' (get) event - set CheckSum"
checksum_value = checksum_row(instance)
logger.debug(f'checksum_value: {checksum_value}')
setattr(instance, "_check_sum_property", checksum_value)
def checksum(list_arg: list) -> str:
"""
Args:
list_arg (list): list of (rows') attribute values (tuple-ize and call hash)
Returns:
int: hash(list values), with special handling for None
"""
real_tuple = []
skip_none = True # work-around for non-repeatable hash(None)
if skip_none: # https://bugs.python.org/issue19224
real_tuple = []
for each_entry in list_arg:
if each_entry is None:
real_tuple.append(13)
else:
if isinstance(each_entry, list):
list_hash = checksum(each_entry)
real_tuple.append(list_hash)
elif isinstance(each_entry, set):
list_from_set = list(each_entry)
list_hash = checksum(list_from_set)
real_tuple.append(list_hash)
elif isinstance(each_entry, dict):
dict_tuple = []
for each_key, each_value in each_entry.items():
dict_tuple.append(each_value)
dict_hash = checksum(dict_tuple)
real_tuple.append(dict_hash)
else:
real_tuple.append(each_entry)
result = hash(tuple(real_tuple))
# print(f'checksum[{result}] from row: {list_arg})')
result = str(result) # maxint 870744036720833075 https://stackoverflow.com/questions/47188449/json-max-int-number
return result
def checksum_row(row: object) -> str:
"""
Args:
row (object): SQLAlchemy row
Returns:
int: hash(row attributes), using checksum()
"""
inspector = inspect(row)
mapper = inspector.mapper
iterate_properties = mapper.iterate_properties
attr_list = []
for each_property in iterate_properties: # does not include CheckSum
if each_property.key == "CheckSum":
logger.debug(f'checksum_row (CheckSum) - good place for breakpoint')
if isinstance(each_property, sqlalchemy.orm.properties.ColumnProperty):
# logger.debug(f'row.property: {each_property} [{getattr(row, each_property.key)}] <{type(each_property)}>')
attr_list.append(getattr(row, each_property.class_attribute.key))
return_value = checksum(attr_list)
# logger.debug(f'checksum_row (get) [{return_value}], inspector: {inspector}')
return return_value
def checksum_old_row(logic_row: object) -> str:
"""
Args:
logic_row (object): old_row (from LogicBank via declare_logic)
Returns:
int: hash(old_row attributes), using checksum()
"""
inspector = inspect(logic_row.row) # get the mapper from row, values from old_row
mapper = inspector.mapper
iterate_properties = mapper.iterate_properties
attr_list = []
for each_property in iterate_properties: # does not include CheckSum
if each_property.key == "CheckSum":
logger.debug(f'checksum_row (CheckSum) - good place for breakpoint')
if isinstance(each_property, sqlalchemy.orm.properties.ColumnProperty):
# logger.debug(f'old_row.property: {each_property} [{getattr(logic_row.old_row, each_property.key)}] <{type(each_property)}>')
attr_list.append(getattr(logic_row.old_row, each_property.class_attribute.key))
return_value = checksum(attr_list)
# logger.debug(f'checksum_row (get) [{return_value}], inspector: {inspector}')
return return_value
class ALSError(JsonapiError):
def __init__(self, message, status_code=HTTPStatus.BAD_REQUEST):
super().__init__()
self.message = message
self.status_code = status_code
def opt_lock_patch(logic_row: LogicRow):
"""
Called by logic/declare_logic in early (before logic) event for updates (patch)
Compares as_read_checksum to old_row_checksum, to determine whether row changed since read
- as_read_checksum is submitted in patch by client, from initial get (see receive_loaded_as_persistent)
- old_row_checksum is provided by Logicbank - it's the current row on disk
Args:
logic_row (LogicRow): LogicBank row being updated
Raises:
ALSError: "Sorry, row altered by another user - please note changes, cancel and retry"
ALSError: "Optimistic Locking error - required CheckSum not present"
"""
logger.debug(f'Opt Lock Patch')
if args.instance.opt_locking == OptLocking.IGNORED.value:
pass
elif hasattr(logic_row.row, "S_CheckSum"):
as_read_checksum = logic_row.row.S_CheckSum
old_row_checksum = checksum_old_row(logic_row)
if as_read_checksum != old_row_checksum:
logger.info(f"optimistic lock failure - as-read vs current: {as_read_checksum} vs {old_row_checksum}")
raise ALSError(message="Sorry, row altered by another user - please note changes, cancel and retry")
else:
if args.instance.opt_locking == OptLocking.OPTIONAL.value:
logger.debug(f'No CheckSum -- ok, configured as optional')
else:
raise ALSError("Optimistic Locking error - required CheckSum not present")