Skip to content

Commit

Permalink
Merge pull request #2474 from Kinto/2472-fix-resource-timestamp-unicity
Browse files Browse the repository at this point in the history
Fix resource timestamp unicity (fixes #2472, #602)
  • Loading branch information
leplatrem committed Jun 26, 2020
2 parents eae57aa + a83ec55 commit f2fa683
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 11 deletions.
4 changes: 2 additions & 2 deletions kinto/core/storage/postgresql/__init__.py
Expand Up @@ -77,7 +77,7 @@ class Storage(StorageBase, MigratorMixin):

# MigratorMixin attributes.
name = "storage"
schema_version = 21
schema_version = 22
schema_file = os.path.join(HERE, "schema.sql")
migrations_directory = os.path.join(HERE, "migrations")

Expand Down Expand Up @@ -201,7 +201,7 @@ def resource_timestamp(self, resource_name, parent_id):
FROM objects
WHERE parent_id = :parent_id
AND resource_name = :resource_name
ORDER BY last_modified DESC
ORDER BY as_epoch(last_modified) DESC
LIMIT 1
)
-- Timestamp of empty resource.
Expand Down
62 changes: 62 additions & 0 deletions kinto/core/storage/postgresql/migrations/migration_021_022.sql
@@ -0,0 +1,62 @@
DROP TRIGGER IF EXISTS tgr_objects_last_modified ON objects;

CREATE OR REPLACE FUNCTION bump_timestamp()
RETURNS trigger AS $$
DECLARE
previous BIGINT;
current BIGINT;
BEGIN
previous := NULL;
WITH existing_timestamps AS (
-- Timestamp of latest record.
(
SELECT last_modified
FROM objects
WHERE parent_id = NEW.parent_id
AND resource_name = NEW.resource_name
ORDER BY as_epoch(last_modified) DESC
LIMIT 1
)
-- Timestamp when resource was empty.
UNION
(
SELECT last_modified
FROM timestamps
WHERE parent_id = NEW.parent_id
AND resource_name = NEW.resource_name
)
)
SELECT as_epoch(MAX(last_modified)) INTO previous
FROM existing_timestamps;

--
-- 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 resource
-- arrive in the same millisecond, the unicity constraint can raise
-- an error (operation is cancelled).
-- See https://github.com/mozilla-services/cliquet/issues/25
--
current := as_epoch(clock_timestamp()::TIMESTAMP);
IF previous IS NOT NULL AND previous >= current THEN
current := previous + 1;
END IF;

IF NEW.last_modified IS NULL OR
(previous IS NOT NULL AND as_epoch(NEW.last_modified) = previous) THEN
-- If record does not carry last-modified, or if the one specified
-- is equal to previous, assign it to current (i.e. bump it).
NEW.last_modified := from_epoch(current);
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tgr_objects_last_modified
BEFORE INSERT OR UPDATE OF data ON objects
FOR EACH ROW EXECUTE PROCEDURE bump_timestamp();

-- Bump storage schema version.
INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '22');
18 changes: 9 additions & 9 deletions kinto/core/storage/postgresql/schema.sql
Expand Up @@ -64,8 +64,8 @@ DROP TRIGGER IF EXISTS tgr_objects_last_modified ON objects;
CREATE OR REPLACE FUNCTION bump_timestamp()
RETURNS trigger AS $$
DECLARE
previous TIMESTAMP;
current TIMESTAMP;
previous BIGINT;
current BIGINT;
BEGIN
previous := NULL;
WITH existing_timestamps AS (
Expand All @@ -75,7 +75,7 @@ BEGIN
FROM objects
WHERE parent_id = NEW.parent_id
AND resource_name = NEW.resource_name
ORDER BY last_modified DESC
ORDER BY as_epoch(last_modified) DESC
LIMIT 1
)
-- Timestamp when resource was empty.
Expand All @@ -87,7 +87,7 @@ BEGIN
AND resource_name = NEW.resource_name
)
)
SELECT MAX(last_modified) INTO previous
SELECT as_epoch(MAX(last_modified)) INTO previous
FROM existing_timestamps;

--
Expand All @@ -99,16 +99,16 @@ BEGIN
-- an error (operation is cancelled).
-- See https://github.com/mozilla-services/cliquet/issues/25
--
current := clock_timestamp();
current := as_epoch(clock_timestamp()::TIMESTAMP);
IF previous IS NOT NULL AND previous >= current THEN
current := previous + INTERVAL '1 milliseconds';
current := previous + 1;
END IF;

IF NEW.last_modified IS NULL OR
(previous IS NOT NULL AND as_epoch(NEW.last_modified) = as_epoch(previous)) THEN
(previous IS NOT NULL AND as_epoch(NEW.last_modified) = previous) THEN
-- If record does not carry last-modified, or if the one specified
-- is equal to previous, assign it to current (i.e. bump it).
NEW.last_modified := current;
NEW.last_modified := from_epoch(current);
END IF;

RETURN NEW;
Expand All @@ -131,4 +131,4 @@ INSERT INTO metadata (name, value) VALUES ('created_at', NOW()::TEXT);

-- Set storage schema version.
-- Should match ``kinto.core.storage.postgresql.PostgreSQL.schema_version``
INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '21');
INSERT INTO metadata (name, value) VALUES ('storage_schema_version', '22');

0 comments on commit f2fa683

Please sign in to comment.