This commit is contained in:
gsinghpal
2026-06-03 19:50:45 -04:00
parent 837198fc8a
commit 2f8b6b3ae0
5 changed files with 169 additions and 25 deletions

View File

@@ -346,15 +346,30 @@ class ResUsers(models.Model):
string='Login Audit Count',
compute='_compute_x_fc_login_audit_count',
)
# NON-STORED on purpose — do NOT re-add store=True.
#
# These were store=True computed-from-the-audit-One2many. That meant every
# successful-login audit row (written through an INDEPENDENT
# registry.cursor(), see _fc_record_login_event) forced a recompute that
# flushed a write-back onto THIS res_users row. During portal-invitation
# acceptance the request has already locked that row (auth_signup just set
# the password in the same transaction), so the audit cursor's write-back
# blocked on the request's own row lock while the request's Python blocked
# waiting for the audit cursor — a self-deadlock Postgres cannot detect
# (the holder shows 'idle in transaction', not lock-waiting). Workers
# wedged for up to limit_time_real (20 min) and odoo-westin went
# unresponsive every time an invite was accepted (issue 2026-06-03).
#
# Keeping them non-stored means creating an audit row never touches
# res_users. They compute on read (display-only on the user form). The
# regression guard is tests.test_last_login_fields_not_stored.
x_fc_last_successful_login = fields.Datetime(
string='Last Successful Login',
compute='_compute_x_fc_last_successful_login',
store=True,
)
x_fc_last_login_ip = fields.Char(
string='Last Login IP', size=45,
compute='_compute_x_fc_last_successful_login',
store=True,
)
@api.depends('x_fc_login_audit_ids')

View File

@@ -303,6 +303,54 @@ class TestFusionLoginAuditModel(TransactionCase):
self.assertGreaterEqual(user.x_fc_login_audit_count, 1)
self.assertEqual(user.x_fc_last_login_ip, '198.51.100.42')
def test_last_login_fields_not_stored(self):
"""Regression guard for the 2026-06-03 invitation-acceptance hang.
x_fc_last_successful_login / x_fc_last_login_ip MUST stay non-stored.
When they were store=True (computed from the audit One2many), creating
the success audit row through the independent registry cursor forced a
write-back onto the very res_users row the request had already locked
(auth_signup had just set the password) -> a self-deadlock Postgres
cannot see (the holder shows 'idle in transaction'). Workers wedged for
up to limit_time_real and odoo-westin became unresponsive whenever an
invitation was accepted. Non-stored means audit-row creation never
touches res_users, so the deadlock cannot form.
"""
fields_ = self.env['res.users']._fields
self.assertFalse(
fields_['x_fc_last_successful_login'].store,
"x_fc_last_successful_login must be non-stored (see docstring)")
self.assertFalse(
fields_['x_fc_last_login_ip'].store,
"x_fc_last_login_ip must be non-stored (see docstring)")
def test_audit_row_create_does_not_write_res_users(self):
"""Creating a login-audit row must not write the linked res_users row.
This is the behavioural half of the deadlock guard: with the fields
non-stored, inserting an audit row for a user leaves that user's
write_date untouched (no recompute -> no res_users UPDATE -> nothing
to contend with the request's own row lock).
"""
user = self.env['res.users'].sudo().create({
'name': 'NoWriteback Tester',
'login': 'nowriteback-tester@example.com',
'password': 'nowriteback-tester-pw-1',
})
user.flush_recordset()
before = user.write_date
self.env['fusion.login.audit'].sudo().create({
'user_id': user.id,
'attempted_login': user.login,
'result': 'success',
'database': self.env.cr.dbname,
'ip_address': '198.51.100.7',
})
user.invalidate_recordset()
self.assertEqual(
user.write_date, before,
"Audit-row create must not write back to res_users")
def test_action_view_login_audit_returns_window_action(self):
"""The smart-button action returns an act_window scoped to this user."""
user = self.env['res.users'].sudo().create({