jaaql-middleware-python 5.3.3__py3-none-any.whl → 5.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jaaql/constants.py CHANGED
@@ -218,5 +218,5 @@ ROLE__dba = "dba"
218
218
 
219
219
  PROTOCOL__postgres = "postgresql://"
220
220
 
221
- VERSION = "5.3.3"
221
+ VERSION = "5.3.5"
222
222
 
jaaql/db/db_interface.py CHANGED
@@ -68,6 +68,11 @@ class DBInterface(ABC):
68
68
  def get_conn(self):
69
69
  pass
70
70
 
71
+ def is_connection_error(self, ex) -> bool:
72
+ # Overridden by the concrete interface. True when a failure means the connection/backend went
73
+ # away (so nothing persisted and the operation is safe to retry on a fresh connection).
74
+ return False
75
+
71
76
  def log_warning(self, exc):
72
77
  if self.logging:
73
78
  logging.warning(exc, exc_info=False)
@@ -77,6 +82,11 @@ class DBInterface(ABC):
77
82
  logging.warning(exc, exc_info=False)
78
83
 
79
84
  def __attempt_commit_rollback(self, conn, err):
85
+ # Returns the commit failure rather than swallowing it. A failed commit means the transaction
86
+ # did NOT persist, so the caller must not report success. Silently swallowing it turned lost
87
+ # writes into a misleading downstream error - e.g. create_account's CREATE ROLE lost on a
88
+ # connection killed by a \wipe dbms reboot, then "GRANT registered TO <role>" failing with
89
+ # "role does not exist" on the very next statement.
80
90
  try:
81
91
  if err is None:
82
92
  self.commit(conn)
@@ -84,9 +94,11 @@ class DBInterface(ABC):
84
94
  self.rollback(conn)
85
95
  except Exception as ex:
86
96
  if err is None:
87
- self.log_warning(ex) # error committing. User stuffed up the SQL
97
+ self.log_warning(ex) # commit failed - the transaction did not persist
98
+ return ex
88
99
  else:
89
100
  self.log_critical(ex) # error rolling back. It is serious
101
+ return None
90
102
 
91
103
  def __err_to_exception(self, err, echo):
92
104
  if err is not None:
@@ -101,19 +113,35 @@ class DBInterface(ABC):
101
113
  self.log_warning(ex) # Serious error, connection failure to db or similar
102
114
  raise ex
103
115
 
116
+ def __raise_commit_failure(self, commit_err):
117
+ # A failed commit persisted nothing; surface it (instead of the old silent swallow) so the
118
+ # caller cannot treat lost writes as success and hit a misleading error further on. A
119
+ # connection-loss failure is raised as a retriable marker so a self-contained caller can
120
+ # re-run on a fresh connection; any other commit failure is a genuine error.
121
+ if self.is_connection_error(commit_err):
122
+ raise ConnectionLostError(str(commit_err))
123
+ raise HttpStatusException("Commit failed, transaction not persisted: " + str(commit_err),
124
+ HTTPStatus.INTERNAL_SERVER_ERROR)
125
+
104
126
  def handle_error(self, conn, err, echo=ECHO__none):
105
- self.__attempt_commit_rollback(conn, err)
127
+ commit_err = self.__attempt_commit_rollback(conn, err)
128
+ if err is None and commit_err is not None:
129
+ self.__raise_commit_failure(commit_err)
106
130
  self.__err_to_exception(err, echo)
107
131
 
108
132
  def put_conn_handle_error(self, conn, err, echo=ECHO__none, skip_rollback_commit: bool = False):
133
+ commit_err = None
109
134
  if not skip_rollback_commit:
110
- self.__attempt_commit_rollback(conn, err)
135
+ commit_err = self.__attempt_commit_rollback(conn, err)
111
136
 
112
137
  try:
113
138
  self.put_conn(conn)
114
139
  except Exception as ex:
115
140
  self.log_warning(ex)
116
141
 
142
+ if err is None and commit_err is not None:
143
+ self.__raise_commit_failure(commit_err)
144
+
117
145
  self.__err_to_exception(err, echo)
118
146
 
119
147
  @abstractmethod
@@ -147,6 +147,21 @@ class DBPGInterface(DBInterface):
147
147
  DBPGInterface.HOST_POOLS = {}
148
148
  DBPGInterface.HOST_POOLS_QUEUES = {}
149
149
 
150
+ @staticmethod
151
+ def check_all_pools():
152
+ # Force a synchronous liveness check of every pooled connection, discarding and replacing any
153
+ # that are broken. clean() reboots Postgres, and background activity (the per-minute cron, the
154
+ # auth verifier) can re-open pools DURING the reboot/reinstall window, caching connections to
155
+ # the old postmaster. psycopg_pool only detects those lazily, so the first real request after
156
+ # the wipe (e.g. \register @dba) gets handed a dead connection whose commit then fails "the
157
+ # connection is lost". Calling this once Postgres is stable makes the refresh deterministic.
158
+ for _, user_pool_dict in DBPGInterface.HOST_POOLS.items():
159
+ for _, pool in user_pool_dict.items():
160
+ try:
161
+ pool.check()
162
+ except Exception:
163
+ pass
164
+
150
165
  @staticmethod
151
166
  def _process_returned_conn(username: str, db_name: str, conn, do_reset: bool):
152
167
  if isinstance(conn, JaaqlPGConnection) and conn.jaaql_has_pending_auth():
@@ -415,6 +430,12 @@ class DBPGInterface(DBInterface):
415
430
  def rollback(self, conn):
416
431
  conn.rollback()
417
432
 
433
+ def is_connection_error(self, ex) -> bool:
434
+ # psycopg raises OperationalError when the backend/connection went away (e.g. terminated by
435
+ # \wipe dbms). The same class the execute retry loop keys off; a commit that fails this way
436
+ # persisted nothing, so the operation is safe to retry on a fresh connection.
437
+ return isinstance(ex, OperationalError)
438
+
418
439
  def handle_db_error(self, err, echo):
419
440
  if isinstance(err, ProgrammingError) and hasattr(err, 'pgresult'):
420
441
  err = err.pgresult.error_message.decode("UTF-8")
jaaql/db/db_utils.py CHANGED
@@ -1,4 +1,4 @@
1
- from jaaql.exceptions.http_status_exception import HttpStatusException, HttpSingletonStatusException
1
+ from jaaql.exceptions.http_status_exception import HttpStatusException, HttpSingletonStatusException, ConnectionLostError
2
2
  from jaaql.interpreter.interpret_jaaql import InterpretJAAQL
3
3
  from jaaql.constants import ENCODING__utf, VAULT_KEY__super_db_credentials
4
4
  from typing import Union
@@ -16,6 +16,10 @@ ERR__expected_single_row = "Expected single row response but received '%d' rows"
16
16
  ERR__unsupported_interface = "Unsupported interface '%s'. We only support %s"
17
17
  ERR__schema_invalid = "Schema invalid!"
18
18
 
19
+ # A lost connection persisted nothing, so a self-contained statement is re-run on a fresh connection.
20
+ # Bounded so a genuinely-down database still surfaces an error rather than looping.
21
+ CONN_LOST__max_attempts = 3
22
+
19
23
  KEY_CONFIG__db = "DATABASE"
20
24
  KEY_CONFIG__interface = "interface"
21
25
  INTERFACE__postgres_key = "postgres"
@@ -104,9 +108,21 @@ def execute_supplied_statement(db_interface, query: str, parameters: dict = None
104
108
  "decrypt": decrypt_columns
105
109
  }
106
110
 
107
- data = InterpretJAAQL(db_interface).transform(statement, skip_commit=skip_commit, encryption_key=encryption_key, autocommit=autocommit,
108
- do_prepare_only=do_prepare_only, attempt_fetch_domain_types=attempt_fetch_domain_types,
109
- psql=psql, pre_psql=pre_psql)
111
+ # Retry only on a lost connection: nothing committed, so re-running on a fresh connection is safe.
112
+ # This closes the gap the deferred-auth optimisation opened - \wipe dbms terminates every backend,
113
+ # and a pooled connection handed out afterwards has no checkout probe and can die at commit. Params
114
+ # are already encrypted above, so the retry re-runs the transform only (never re-encrypts).
115
+ attempts = 0
116
+ while True:
117
+ attempts += 1
118
+ try:
119
+ data = InterpretJAAQL(db_interface).transform(statement, skip_commit=skip_commit, encryption_key=encryption_key, autocommit=autocommit,
120
+ do_prepare_only=do_prepare_only, attempt_fetch_domain_types=attempt_fetch_domain_types,
121
+ psql=psql, pre_psql=pre_psql)
122
+ break
123
+ except ConnectionLostError:
124
+ if attempts >= CONN_LOST__max_attempts:
125
+ raise
110
126
 
111
127
  if as_objects:
112
128
  data = objectify(data)
@@ -34,6 +34,15 @@ class HttpSingletonStatusException(HttpStatusException):
34
34
  self.actual_count = actual_count
35
35
 
36
36
 
37
+ class ConnectionLostError(HttpStatusException):
38
+ # Raised when a database operation failed because its connection was lost (e.g. the backend was
39
+ # terminated by \wipe dbms's pg_terminate_backend) BEFORE anything committed. Nothing persisted,
40
+ # so a self-contained operation may safely be retried on a fresh connection. Subclasses
41
+ # HttpStatusException so that if retries are exhausted it still surfaces as a clean 500.
42
+ def __init__(self, message: str):
43
+ super().__init__("Commit failed, transaction not persisted: " + message, HTTPStatus.INTERNAL_SERVER_ERROR)
44
+
45
+
37
46
  class JaaqlInterpretableHandledError(Exception):
38
47
  def __init__(self, error_code: int, http_response_code: int,
39
48
  table_name: str | None, index: int | None, message: str,
jaaql/mvc/model.py CHANGED
@@ -1457,6 +1457,11 @@ WHERE
1457
1457
  self.vault.get_obj(VAULT_KEY__jaaql_db_password)
1458
1458
  )
1459
1459
 
1460
+ # Postgres was just rebooted and reinstalled. Discard any connections opened against the old
1461
+ # postmaster during that window (by the cron/verifier/install) so the next request cannot be
1462
+ # handed a dead connection whose commit silently fails. Runs once per wipe - no per-request cost.
1463
+ DBPGInterface.check_all_pools()
1464
+
1460
1465
  def execute_migrations(self, connection: DBInterface):
1461
1466
  self.is_super_admin(connection)
1462
1467
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 5.3.3
3
+ Version: 5.3.5
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -1,6 +1,6 @@
1
1
  jaaql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  jaaql/config_constants.py,sha256=puoR6jfbXw8oXXEfbYNn2ysGwgiGTyuDjOj7F8acsTE,470
3
- jaaql/constants.py,sha256=ocgDdVs0zked7wQ2_gh_LHKk9KO4rujsTTnNc2mI_Qc,7355
3
+ jaaql/constants.py,sha256=OgkEu5fPTtBRNfs7xo-b6m4tyeC7S4Uc39CDkGaOqlI,7355
4
4
  jaaql/generated_constants.py,sha256=pxmpTux7rrjJH6iqLb-R8RKJ4t0CeoGdnspjH5DiWgU,11327
5
5
  jaaql/jaaql.py,sha256=bfiEW9A8PlZIVjDJd_LOhCLBL8flc_f7w0umR6EWbU4,6614
6
6
  jaaql/patch.py,sha256=VE4B6jA492NCIxNIewbynh92jxVulmRFtB9hNEe6q94,149
@@ -9,9 +9,9 @@ jaaql/config/config-docker.ini,sha256=2UNtdwHQWZhANgxnrHvtxnDW0C0MuX042IZITMXNsZ
9
9
  jaaql/config/config-test.ini,sha256=ap9OFqCCbYdWqmUKq0p9GD1IkM4CzeTXDV5gbQGQZoQ,283
10
10
  jaaql/config/config.ini,sha256=o1orXNYRFgf09MKzZBQ3v0oMzWn7N6KwvX5YV0WJtns,321
11
11
  jaaql/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- jaaql/db/db_interface.py,sha256=n4pwRHeGpkbmTOvZDe8VvqFf2l4QsE9GbhUBzW335m0,7702
13
- jaaql/db/db_pg_interface.py,sha256=zBZUkfbbmN8WU6gz0vLiZv10QKd1e7muhx0zP6yekt4,21098
14
- jaaql/db/db_utils.py,sha256=xZCdtWT9semzLL-NAiwMFx_38Wv96BBv60pfPYNDnCc,7761
12
+ jaaql/db/db_interface.py,sha256=Zc6ajjjGMEZGH0BU6kIOhanvQEHSXlNIAgPwdvSaIdQ,9436
13
+ jaaql/db/db_pg_interface.py,sha256=heaOexurUHjZDgsfOGPiChE_cTJCipw2gOJp93tC6SY,22367
14
+ jaaql/db/db_utils.py,sha256=vi5V9FCybv1Yeef9glJmJrj6Ca_oQiJoOQ1rAN1uGKc,8623
15
15
  jaaql/db/db_utils_no_circ.py,sha256=WLhk6EwsnTdLGUAjq3RZBH3FzgwEumh45A24WWysWmQ,4666
16
16
  jaaql/documentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  jaaql/documentation/documentation_internal.py,sha256=_tU92TI4Sd8AXj3b7-UOKCN1h4VR7SaakIv2ATC45Bo,22783
@@ -23,7 +23,7 @@ jaaql/email/email_manager_service.py,sha256=if8SnD1VdN2pKBwVsj8Y0aYgV3wiFJrHqZvA
23
23
  jaaql/email/patch_ems.py,sha256=WGAUI6hSjsQFfLz-naM_0xy3o0DGYXuQTs-TDXFgJaI,234
24
24
  jaaql/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  jaaql/exceptions/custom_http_status.py,sha256=k-8Uabjtl7NgeFxyNPAsMk4HkKMz0Z6SZwwweFbxqyg,417
26
- jaaql/exceptions/http_status_exception.py,sha256=rTFegFibZ9C_G3BzdLCACOkZc3k1VNSHPjxZuutToAg,2200
26
+ jaaql/exceptions/http_status_exception.py,sha256=lD-eI_C9kHmThFP94ARiGvBQdTjbEM2NecJ-RvR41ds,2789
27
27
  jaaql/exceptions/jaaql_interpretable_handled_errors.py,sha256=846c3MLeQ9VQjs9f4WM6H4rszh4tEGDLgBRd8nEMCc8,11180
28
28
  jaaql/exceptions/not_yet_implement_exception.py,sha256=T7QC-I7ciWuFFp7C_jIJRMDlpshVdy_01bVuEnu-Txs,321
29
29
  jaaql/interpreter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -38,7 +38,7 @@ jaaql/mvc/controller_interface.py,sha256=Wx8fA4lrqb_PPwkLNjJDxDloHB5JxmNt1Gv3SsD
38
38
  jaaql/mvc/exception_queries.py,sha256=lF65cj9KgJgRvyzxDjPDM05rHFSHF0jy1V8B77H9Rvo,7899
39
39
  jaaql/mvc/generated_queries.py,sha256=z1Jsyu-Y6D-Xd8LYvefzwv8C01NaBjHaHI7mcvv-WEk,72232
40
40
  jaaql/mvc/handmade_queries.py,sha256=kyXZ7L_AoxfcPR4u3r5So8khlRD6_sGQ39JnSAHDsEA,938
41
- jaaql/mvc/model.py,sha256=NAKI651kg8HdMY-4IaxkZbussKRo0b-M34cT3Bz2l-U,132860
41
+ jaaql/mvc/model.py,sha256=pPAu4GregD-XxVSCMjdqxhnTRoGJh6iqXqXF0yba4OA,133209
42
42
  jaaql/mvc/model_interface.py,sha256=YgAjaAiIQL1UtIJYgvkl_C3r6PVo-8Z3MXtwzyIwKnc,257
43
43
  jaaql/mvc/response.py,sha256=F4WvR9h1JcWF3Y6cj3n5HpVIqnwiMpNqfE8D29ZHbSE,860
44
44
  jaaql/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -66,8 +66,8 @@ jaaql/utilities/options.py,sha256=75M7-oSeY4wmHGz3-su_c36yf_Csv9HxvTG7aRlGR0A,49
66
66
  jaaql/utilities/utils.py,sha256=nVYVhuHtWSjdaJaXe9eC1Usja56NO-V9SoBIlcHAK8Q,5166
67
67
  jaaql/utilities/utils_no_project_imports.py,sha256=YwcskR15dAqKdEiuF8zGx-tzwWkclSGil_zm_qZ8ylU,4626
68
68
  jaaql/utilities/vault.py,sha256=DudyQNtMtaRLL71IGQX0AI16Tg1id98BHTCQPyUNkmE,2243
69
- jaaql_middleware_python-5.3.3.dist-info/licenses/LICENSE.txt,sha256=wr50eN0TnkrCSuNwyiI81FLZqmrW3q0JqRlVVQIrGA0,18134
70
- jaaql_middleware_python-5.3.3.dist-info/METADATA,sha256=oEe5Uk6pBPUmldYY9N9IKLjQU-XWreZiDXBGCxkZusw,1976
71
- jaaql_middleware_python-5.3.3.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
72
- jaaql_middleware_python-5.3.3.dist-info/top_level.txt,sha256=k5aKVQy77bPyfyNHITvo5m3FqdSAiiXbjT5S9D9KzgU,6
73
- jaaql_middleware_python-5.3.3.dist-info/RECORD,,
69
+ jaaql_middleware_python-5.3.5.dist-info/licenses/LICENSE.txt,sha256=wr50eN0TnkrCSuNwyiI81FLZqmrW3q0JqRlVVQIrGA0,18134
70
+ jaaql_middleware_python-5.3.5.dist-info/METADATA,sha256=VvWDwa-IeIYCx_HkIja6GyabnVLzHzx87FDb3tQ5dvY,1976
71
+ jaaql_middleware_python-5.3.5.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
72
+ jaaql_middleware_python-5.3.5.dist-info/top_level.txt,sha256=k5aKVQy77bPyfyNHITvo5m3FqdSAiiXbjT5S9D9KzgU,6
73
+ jaaql_middleware_python-5.3.5.dist-info/RECORD,,