-
Notifications
You must be signed in to change notification settings - Fork 15
Keep last modified #604
Keep last modified #604
Changes from 9 commits
7575d48
4f2430b
b3d017d
8700c8d
53f68b3
5cf0476
79f03df
4ed2b6c
791f7d6
5e200d6
e60ccfa
5c367dc
27c2b8e
e3549f3
1e92ca8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -492,7 +492,11 @@ def delete(self): | |
record = self._get_record_or_404(self.record_id) | ||
self._raise_412_if_modified(record) | ||
|
||
deleted = self.model.delete_record(record) | ||
# Retreive the last_modified information from a querystring if present. | ||
last_modified = self.request.GET.get('last_modified') | ||
|
||
deleted = self.model.delete_record( | ||
record, last_modified=last_modified) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think elsewhere we do this: deleted = self.model.delete_record(record,
last_modified=last_modified) |
||
return self.postprocess(deleted, action=ACTIONS.DELETE) | ||
|
||
# | ||
|
@@ -528,6 +532,15 @@ def process_record(self, new, old=None): | |
:returns: the processed record. | ||
:rtype: dict | ||
""" | ||
if old is None or self.model.modified_field not in old: | ||
return new | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me realize that a |
||
|
||
# Drop the new last_modified if lesser or equal to the old one. | ||
new_last_modified = new.get(self.model.modified_field) | ||
if (new_last_modified and | ||
new_last_modified <= old[self.model.modified_field]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: please use intermediary variable to avoid indenting in if conditions |
||
del new[self.model.modified_field] | ||
|
||
return new | ||
|
||
def apply_changes(self, record, changes): | ||
|
@@ -1069,6 +1082,7 @@ def process_record(self, new, old=None): | |
"""Read permissions from request body, and in the case of ``PUT`` every | ||
existing ACE is removed (using empty list). | ||
""" | ||
new = super(ShareableResource, self).process_record(new, old) | ||
permissions = self.request.validated.get('permissions', {}) | ||
|
||
annotated = new.copy() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,8 +55,11 @@ def strip_deleted_record(self, resource, parent_id, record, | |
return deleted | ||
|
||
def set_record_timestamp(self, collection_id, parent_id, record, | ||
modified_field=DEFAULT_MODIFIED_FIELD): | ||
timestamp = self._bump_timestamp(collection_id, parent_id) | ||
modified_field=DEFAULT_MODIFIED_FIELD, | ||
last_modified=None): | ||
timestamp = self._bump_timestamp(collection_id, parent_id, record, | ||
modified_field, | ||
last_modified=last_modified) | ||
record[modified_field] = timestamp | ||
return record | ||
|
||
|
@@ -171,7 +174,8 @@ def collection_timestamp(self, collection_id, parent_id, auth=None): | |
return ts | ||
return self._bump_timestamp(collection_id, parent_id) | ||
|
||
def _bump_timestamp(self, collection_id, parent_id): | ||
def _bump_timestamp(self, collection_id, parent_id, record=None, | ||
modified_field=None, last_modified=None): | ||
"""Timestamp are base on current millisecond. | ||
|
||
.. note :: | ||
|
@@ -180,11 +184,28 @@ def _bump_timestamp(self, collection_id, parent_id): | |
the time will slide into the future. It is not problematic since | ||
the timestamp notion is opaque, and behaves like a revision number. | ||
""" | ||
is_specified = (record is not None | ||
and modified_field in record | ||
or last_modified is not None) | ||
if is_specified: | ||
# If there is a timestamp in the new record, try to use it. | ||
if last_modified is not None: | ||
current = last_modified | ||
else: | ||
current = record[modified_field] | ||
else: | ||
current = utils.msec_time() | ||
|
||
previous = self._timestamps[collection_id].get(parent_id) | ||
current = utils.msec_time() | ||
if previous and previous >= current: | ||
current = previous + 1 | ||
self._timestamps[collection_id][parent_id] = current | ||
collection_timestamp = previous + 1 | ||
else: | ||
collection_timestamp = current | ||
|
||
if not is_specified: | ||
current = collection_timestamp | ||
|
||
self._timestamps[collection_id][parent_id] = collection_timestamp | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Since the flow of this function is a bit complex, I would suggest putting a summary of the spec in the docstring (or a bit more comments). |
||
return current | ||
|
||
def create(self, collection_id, parent_id, record, id_generator=None, | ||
|
@@ -233,10 +254,15 @@ def delete(self, collection_id, parent_id, object_id, | |
id_field=DEFAULT_ID_FIELD, with_deleted=True, | ||
modified_field=DEFAULT_MODIFIED_FIELD, | ||
deleted_field=DEFAULT_DELETED_FIELD, | ||
auth=None): | ||
auth=None, last_modified=None): | ||
existing = self.get(collection_id, parent_id, object_id) | ||
# Need to delete the last_modified field of the record for it to not | ||
# be kept (in no case we want to keep the old one). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: for it to not be kept is not really useful after need to delete |
||
del existing[modified_field] | ||
|
||
self.set_record_timestamp(collection_id, parent_id, existing, | ||
modified_field=modified_field) | ||
modified_field=modified_field, | ||
last_modified=last_modified) | ||
existing = self.strip_deleted_record(collection_id, | ||
parent_id, | ||
existing) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
CREATE OR REPLACE FUNCTION from_epoch(epoch BIGINT) RETURNS TIMESTAMP AS $$ | ||
BEGIN | ||
RETURN TIMESTAMP WITH TIME ZONE 'epoch' + epoch * INTERVAL '1 millisecond'; | ||
END; | ||
$$ LANGUAGE plpgsql | ||
IMMUTABLE; | ||
|
||
|
||
CREATE OR REPLACE FUNCTION bump_timestamp() | ||
RETURNS trigger AS $$ | ||
DECLARE | ||
previous TIMESTAMP; | ||
current TIMESTAMP; | ||
|
||
BEGIN | ||
-- | ||
-- This bumps the current timestamp to 1 msec in the future if the previous | ||
-- timestamp is equal to the current one (or higher if was bumped already). | ||
-- | ||
-- If a bunch of requests from the same user on the same collection | ||
-- arrive in the same millisecond, the unicity constraint can raise | ||
-- an error (operation is cancelled). | ||
-- See https://github.com/mozilla-services/cliquet/issues/25 | ||
-- | ||
previous := collection_timestamp(NEW.parent_id, NEW.collection_id); | ||
|
||
IF NEW.last_modified IS NULL THEN | ||
current := clock_timestamp(); | ||
IF previous >= current THEN | ||
current := previous + INTERVAL '1 milliseconds'; | ||
END IF; | ||
NEW.last_modified := current; | ||
END IF; | ||
|
||
RETURN NEW; | ||
END; | ||
$$ LANGUAGE plpgsql; | ||
|
||
|
||
-- Bump storage schema version. | ||
INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '9'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,13 @@ END; | |
$$ LANGUAGE plpgsql | ||
IMMUTABLE; | ||
|
||
CREATE OR REPLACE FUNCTION from_epoch(epoch BIGINT) RETURNS TIMESTAMP AS $$ | ||
BEGIN | ||
RETURN TIMESTAMP WITH TIME ZONE 'epoch' + epoch * INTERVAL '1 millisecond'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this handle epoch being null? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently it does 👍 |
||
END; | ||
$$ LANGUAGE plpgsql | ||
IMMUTABLE; | ||
|
||
-- | ||
-- Actual records | ||
-- | ||
|
@@ -161,6 +168,7 @@ RETURNS trigger AS $$ | |
DECLARE | ||
previous TIMESTAMP; | ||
current TIMESTAMP; | ||
|
||
BEGIN | ||
-- | ||
-- This bumps the current timestamp to 1 msec in the future if the previous | ||
|
@@ -172,14 +180,15 @@ BEGIN | |
-- See https://github.com/mozilla-services/cliquet/issues/25 | ||
-- | ||
previous := collection_timestamp(NEW.parent_id, NEW.collection_id); | ||
current := clock_timestamp(); | ||
|
||
IF previous >= current THEN | ||
current := previous + INTERVAL '1 milliseconds'; | ||
IF NEW.last_modified IS NULL THEN | ||
current := clock_timestamp(); | ||
IF previous >= current THEN | ||
current := previous + INTERVAL '1 milliseconds'; | ||
END IF; | ||
NEW.last_modified := current; | ||
END IF; | ||
|
||
NEW.last_modified := current; | ||
|
||
RETURN NEW; | ||
END; | ||
$$ LANGUAGE plpgsql; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,19 +88,42 @@ def collection_timestamp(self, collection_id, parent_id, auth=None): | |
return self._bump_timestamp(collection_id, parent_id) | ||
|
||
@wrap_redis_error | ||
def _bump_timestamp(self, collection_id, parent_id): | ||
def _bump_timestamp(self, collection_id, parent_id, record=None, | ||
modified_field=None, last_modified=None): | ||
|
||
key = '{0}.{1}.timestamp'.format(collection_id, parent_id) | ||
while 1: | ||
with self._client.pipeline() as pipe: | ||
try: | ||
pipe.watch(key) | ||
previous = pipe.get(key) | ||
pipe.multi() | ||
current = utils.msec_time() | ||
is_specified = (record is not None | ||
and modified_field in record | ||
or last_modified is not None) | ||
if is_specified: | ||
# If there is a timestamp in the new record, | ||
# try to use it. | ||
if last_modified is not None: | ||
current = last_modified | ||
else: | ||
current = record[modified_field] | ||
else: | ||
current = utils.msec_time() | ||
|
||
if previous and int(previous) >= current: | ||
current = int(previous) + 1 | ||
pipe.set(key, current) | ||
collection_timestamp = int(previous) + 1 | ||
else: | ||
collection_timestamp = current | ||
|
||
# Return the newly generated timestamp as the current one | ||
# only if nothing else was specified. | ||
if not is_specified: | ||
current = collection_timestamp | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since |
||
|
||
print(is_specified, current, collection_timestamp, record) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. print() left |
||
|
||
pipe.set(key, collection_timestamp) | ||
pipe.execute() | ||
return current | ||
except redis.WatchError: # pragma: no cover | ||
|
@@ -192,7 +215,7 @@ def delete(self, collection_id, parent_id, object_id, | |
id_field=DEFAULT_ID_FIELD, with_deleted=True, | ||
modified_field=DEFAULT_MODIFIED_FIELD, | ||
deleted_field=DEFAULT_DELETED_FIELD, | ||
auth=None): | ||
auth=None, last_modified=None): | ||
record_key = '{0}.{1}.{2}.records'.format(collection_id, | ||
parent_id, | ||
object_id) | ||
|
@@ -211,8 +234,13 @@ def delete(self, collection_id, parent_id, object_id, | |
|
||
existing = self._decode(encoded_item) | ||
|
||
# Need to delete the last_modified field of the record for it to not | ||
# be kept (in no case we want to keep the old one). | ||
del existing[modified_field] | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Need to remove whitespaces. for flake8 |
||
self.set_record_timestamp(collection_id, parent_id, existing, | ||
modified_field=modified_field) | ||
modified_field=modified_field, | ||
last_modified=last_modified) | ||
existing = self.strip_deleted_record(collection_id, parent_id, | ||
existing) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: do not forget to update the API docs with a
.. versionadded::
section