Remove `with_lockmode` use from Trust SQL backend.
Due to the lack of support of `SELECT .. FOR UPDATE` call in
MySQL + Galera, the use of `with_lockmode('update')` needs to be
removed from the Trust SQL backend when utilizing the MySQL
dialect.
Instead of the pessimistic locking method (row lock, table lock),
use optimistic locking. The consume_use method now attempts to
update the remaining_uses column for the trust in question only
if the remaining_uses equal the same number as on the initial
query. This is done in a loop with a 10-iteration failsafe to
prevent endless looping.
Conflicts:
keystone/trust/backends/sql.py
Minor changes to the sql.py driver over the course of Juno
caused the conflict. The resulting code for consume has
not changed.
Change-Id: I1b8af6ce5709f829f345cd351ec9242d0217e743
Closes-Bug: #1325143
(cherry-picked from commit 0724fc4d27
)
This commit is contained in:
parent
6cbf835542
commit
8485dbc4d6
|
@ -303,6 +303,11 @@ class UnexpectedError(SecurityError):
|
|||
title = 'Internal Server Error'
|
||||
|
||||
|
||||
class TrustConsumeMaximumAttempt(UnexpectedError):
|
||||
debug_message_format = _("Unable to consume trust %(trust_id)s, unable to "
|
||||
"acquire lock.")
|
||||
|
||||
|
||||
class CertificateFilesUnavailable(UnexpectedError):
|
||||
debug_message_format = _("Expected signing certificates are not available "
|
||||
"on the server. Please check Keystone "
|
||||
|
|
|
@ -12,12 +12,21 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import time
|
||||
|
||||
from keystone.common import sql
|
||||
from keystone import exception
|
||||
from keystone.openstack.common import log
|
||||
from keystone.openstack.common import timeutils
|
||||
from keystone import trust
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
# The maximum number of iterations that will be attempted for optimistic
|
||||
# locking on consuming a limited-use trust.
|
||||
MAXIMUM_CONSUME_ATTEMPTS = 10
|
||||
|
||||
|
||||
class TrustModel(sql.ModelBase, sql.DictBase):
|
||||
__tablename__ = 'trust'
|
||||
attributes = ['id', 'trustor_user_id', 'trustee_user_id',
|
||||
|
@ -72,21 +81,51 @@ class Trust(trust.Driver):
|
|||
|
||||
@sql.handle_conflicts(conflict_type='trust')
|
||||
def consume_use(self, trust_id):
|
||||
session = sql.get_session()
|
||||
with session.begin():
|
||||
ref = (session.query(TrustModel).
|
||||
with_lockmode('update').
|
||||
filter_by(deleted_at=None).
|
||||
filter_by(id=trust_id).first())
|
||||
if ref is None:
|
||||
raise exception.TrustNotFound(trust_id=trust_id)
|
||||
if ref.remaining_uses is None:
|
||||
# unlimited uses, do nothing
|
||||
pass
|
||||
elif ref.remaining_uses > 0:
|
||||
ref.remaining_uses -= 1
|
||||
else:
|
||||
raise exception.TrustUseLimitReached(trust_id=trust_id)
|
||||
for attempt in range(MAXIMUM_CONSUME_ATTEMPTS):
|
||||
with sql.transaction() as session:
|
||||
try:
|
||||
query_result = (session.query(TrustModel.remaining_uses).
|
||||
filter_by(id=trust_id).
|
||||
filter_by(deleted_at=None).one())
|
||||
except sql.NotFound:
|
||||
raise exception.TrustNotFound(trust_id=trust_id)
|
||||
|
||||
remaining_uses = query_result.remaining_uses
|
||||
|
||||
if remaining_uses is None:
|
||||
# unlimited uses, do nothing
|
||||
break
|
||||
elif remaining_uses > 0:
|
||||
# NOTE(morganfainberg): use an optimistic locking method
|
||||
# to ensure we only ever update a trust that has the
|
||||
# expected number of remaining uses.
|
||||
rows_affected = (
|
||||
session.query(TrustModel).
|
||||
filter_by(id=trust_id).
|
||||
filter_by(deleted_at=None).
|
||||
filter_by(remaining_uses=remaining_uses).
|
||||
update({'remaining_uses': (remaining_uses - 1)},
|
||||
synchronize_session=False))
|
||||
if rows_affected == 1:
|
||||
# Successfully consumed a single limited-use trust.
|
||||
# Since trust_id is the PK on the Trust table, there is
|
||||
# no case we should match more than 1 row in the
|
||||
# update. We either update 1 row or 0 rows.
|
||||
break
|
||||
else:
|
||||
raise exception.TrustUseLimitReached(trust_id=trust_id)
|
||||
# NOTE(morganfainberg): Ensure we have a yield point for eventlet
|
||||
# here. This should cost us nothing otherwise. This can be removed
|
||||
# if/when oslo.db cleanly handles yields on db calls.
|
||||
time.sleep(0)
|
||||
else:
|
||||
# NOTE(morganfainberg): In the case the for loop is not prematurely
|
||||
# broken out of, this else block is executed. This means the trust
|
||||
# was not unlimited nor was it consumed (we hit the maximum
|
||||
# iteration limit). This is just an indicator that we were unable
|
||||
# to get the optimistic lock rather than silently failing or
|
||||
# incorrectly indicating a trust was consumed.
|
||||
raise exception.TrustConsumeMaximumAttempt(trust_id=trust_id)
|
||||
|
||||
def get_trust(self, trust_id):
|
||||
session = sql.get_session()
|
||||
|
|
Loading…
Reference in New Issue