Add atomic conditional updates to objects
To allow atomic state changes across Cinder services we need to
implement a way to easily do compare-and-swap.
This patch adds methods to allow compare-and-swap on DB models and on
Cinder Versioned Objects as well.
Conditions for the compare part of the update can consist of:
- Inclusion: status == 'available'
- Exclusion: status != 'in-use'
- Multi-inclusion: status in ('available', 'error')
- Multi-exclusion: status not in ('attaching', 'in-use')
- Sqlalchemy filters
A complete example of usage would be the compare-and-swap used in volume
delete requests that has to take in consideration not only the status
but the attach and migration status as well as the volume not having
snapshots:
now = timeutils.utcnow()
expected = {'attach_status': db.Not('attached'),
'migration_status': None,
'consistencygroup_id': None}
good_status = ('available', 'error', 'error_restoring',
'error_extending')
if not force:
expected.update(status=good_status)
# Volume cannot have snapshots if we want to delete it
filters = [~sql.exists().where(models.Volume.id ==
models.Snapshot.volume_id)]
updated = vol_obj.conditional_update(
{'status': 'deleting',
'previous_status': vol_obj.model.status,
'terminated_at': now},
expected,
filters)
It can also be specified whether to save already dirtied fields from the
objects or not and by default (if no expected_values argument is
provided) it will make sure that the entry in the DB has the same values
as the objects we are saving.
We can select values based on conditions using Case objects in the
'values' argument. For example:
has_snapshot_filter = sql.exists().where(
models.Snapshot.volume_id == models.Volume.id)
case_values = volume.Case([(has_snapshot_filter, 'has-snapshot')],
else_='no-snapshot')
volume.conditional_update({'status': case_values},
{'status': 'available'})
Exclusion and multi-exclusion will handle, by default, NULL values like
Python does instead of like SQL does, so NULL values will be considered
different than any non NULL values. That way if we search for something
not equal to 1 we will also get NULL values.
WARNING: SQLAlchemy does not allow selecting order of SET clauses, so
for now we cannot do things like
{'previous_status': model.status, 'status': 'retyping'}
because it will result in both previous_status and status being set to
'retyping'. Issue has been reported [1] and a patch to fix it [2] has
been submitted.
[1]: https://bitbucket.org/zzzeek/sqlalchemy/issues/3541
[2]: https://github.com/zzzeek/sqlalchemy/pull/200
Specs: https://review.openstack.org/232599/
Implements: blueprint cinder-volume-active-active-support
Related-Bug: #
1490944
Related-Bug: #
1238093
Related-Bug: #
1490946
Related-Bug: #
1469659
Related-Bug: #
1493120
Related-Bug: #
1493419
Related-Bug: #
1493476
Related-Bug: #
1494466
Change-Id: If90a37f8c7d6fad8fc1f861d52ba862875920cdc