Skip to content

Geo: Resync direct upload object stored artifacts

Michael Kozono requested to merge mk/geo-resync-direct-uploaded-artifacts into master

What does this MR do and why?

This MR causes secondary Geo sites to resync job artifacts which were potentially affected by #419742 (closed).

I aim to backport this MR to 16.3, 16.2, and ideally 16.1, to ensure that customers can upgrade to any latest patch release to automatically avoid data loss.

Note that Geo cannot use Batched Background Migrations #353644

Migration output

Up

If object storage or direct upload is not configured, then it does nothing:

WARNING: Could not write to the database ci: cannot execute UPSERT in a read-only transaction
WARNING: Could not write to the database main: cannot execute UPSERT in a read-only transaction
geo: == [advisory_lock_connection] object_id: 225600, pg_backend_pid: 70141
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrating ============
geo: -- Skipping because job artifacts are not stored in object storage with direct upload
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrated (0.0002s) ===

geo: == [advisory_lock_connection] object_id: 225600, pg_backend_pid: 70141

If the secondary Geo site is not configured to replicate object storage, then it does nothing:

WARNING: Could not write to the database ci: cannot execute UPSERT in a read-only transaction
WARNING: Could not write to the database main: cannot execute UPSERT in a read-only transaction
geo: == [advisory_lock_connection] object_id: 225180, pg_backend_pid: 30902
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrating ============
geo: -- Skipping because this Geo site does not replicate object storage
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrated (0.0014s) ===

geo: == [advisory_lock_connection] object_id: 225180, pg_backend_pid: 30902

If there are artifacts that may be affected, then it marks the registry rows as needing to be synced (via Sidekiq jobs):

geo: == [advisory_lock_connection] object_id: 257000, pg_backend_pid: 48361
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrating ============
geo: -- Enqueuing Geo::ResyncDirectUploadJobArtifactRegistryWorker jobs to mark artifacts pending
geo: -- See https://gitlab.com/gitlab-org/gitlab/-/issues/419742
geo: -- Enqueued 1 Geo::ResyncDirectUploadJobArtifactRegistryWorker job
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: migrated (0.1244s) ===

geo: == [advisory_lock_connection] object_id: 257000, pg_backend_pid: 48361

In development or test environments, the loop in the migration also outputs the range of each enqueued job:

geo: -- Enqueuing Geo::ResyncDirectUploadJobArtifactRegistryWorker for range 54..810

Down

geo: == [advisory_lock_connection] object_id: 223280, pg_backend_pid: 49037
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: reverting ============
geo: == 20230808031211 ResyncDirectUploadJobArtifactRegistry: reverted (0.0005s) ===

Query analysis

Executed locally since GitLab.com doesn't use Geo, and staging-ref only has 700 artifacts anyway. The each_batch_range scopes work only with the primary key, so I expect performance of each loop is fast. The real work is in the UPDATE queries, and those are performed async, and they are scoped to a range on the primary key.

Side note: I originally scoped each_batch_range to where state = 2, because I expected job_artifact_registry to have an index on state. But it does not have that index. I opened an issue about that. A scope on state is not crucial for this migration though, so I removed it.

each_batch_range query 1 https://explain.depesz.com/s/gOlK:

➜  gitlab git:(49b2f611eeef) ✗ gdk psql-geo
psql (13.9)
Type "help" for help.

gitlabhq_geo_development=# explain analyze SELECT "job_artifact_registry"."id" FROM "job_artifact_registry" ORDER BY "job_artifact_registry"."id" ASC LIMIT 1;
                                                                          QUERY PLAN                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.14..0.26 rows=1 width=4) (actual time=0.026..0.027 rows=1 loops=1)
   ->  Index Only Scan using job_artifact_registry_pkey on job_artifact_registry  (cost=0.14..9.34 rows=80 width=4) (actual time=0.025..0.025 rows=1 loops=1)
         Heap Fetches: 0
 Planning Time: 0.863 ms
 Execution Time: 0.051 ms
(5 rows)

each_batch_range query 2 https://explain.depesz.com/s/xL88:

gitlabhq_geo_development=# explain analyze SELECT "job_artifact_registry"."id" FROM "job_artifact_registry" WHERE "job_artifact_registry"."id" >= 17 ORDER BY "job_artifact_registry"."id" ASC LIMIT 1 OFFSET 10000;
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=7.12..7.12 rows=1 width=4) (actual time=0.083..0.083 rows=0 loops=1)
   ->  Sort  (cost=6.96..7.12 rows=65 width=4) (actual time=0.076..0.079 rows=64 loops=1)
         Sort Key: id
         Sort Method: quicksort  Memory: 28kB
         ->  Seq Scan on job_artifact_registry  (cost=0.00..5.00 rows=65 width=4) (actual time=0.023..0.052 rows=64 loops=1)
               Filter: (id >= 17)
               Rows Removed by Filter: 16
 Planning Time: 0.157 ms
 Execution Time: 0.103 ms
(9 rows)

each_batch_range query 3 https://explain.depesz.com/s/CHxt:

gitlabhq_geo_development=# explain analyze SELECT MIN(id), MAX(id) FROM "job_artifact_registry" WHERE "job_artifact_registry"."id" >= 17 LIMIT 1;
                                                                                              QUERY PLAN                                                                                               
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.57..0.58 rows=1 width=8) (actual time=0.078..0.080 rows=1 loops=1)
   InitPlan 1 (returns $0)
     ->  Limit  (cost=0.14..0.29 rows=1 width=4) (actual time=0.022..0.023 rows=1 loops=1)
           ->  Index Only Scan using job_artifact_registry_pkey on job_artifact_registry  (cost=0.14..9.44 rows=65 width=4) (actual time=0.021..0.022 rows=1 loops=1)
                 Index Cond: ((id IS NOT NULL) AND (id >= 17))
                 Heap Fetches: 0
   InitPlan 2 (returns $1)
     ->  Limit  (cost=0.14..0.29 rows=1 width=4) (actual time=0.050..0.050 rows=1 loops=1)
           ->  Index Only Scan Backward using job_artifact_registry_pkey on job_artifact_registry job_artifact_registry_1  (cost=0.14..9.44 rows=65 width=4) (actual time=0.049..0.049 rows=1 loops=1)
                 Index Cond: ((id IS NOT NULL) AND (id >= 17))
                 Heap Fetches: 0
   ->  Result  (cost=0.57..0.58 rows=1 width=8) (actual time=0.077..0.077 rows=1 loops=1)
 Planning Time: 0.732 ms
 Execution Time: 0.118 ms
(14 rows)

Executed by the background job https://explain.depesz.com/s/ngJF:

gitlabhq_geo_development=# explain analyze UPDATE job_artifact_registry SET state = 0, last_synced_at = NULL WHERE state = 2 AND last_synced_at BETWEEN '2023-06-22T00:00:00' AND '2024-02-03T00:00:00' AND id BETWEEN 1 AND 10000;
                                                                                                     QUERY PLAN                                                                                                      
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Update on job_artifact_registry  (cost=0.00..5.80 rows=74 width=796) (actual time=0.972..0.973 rows=0 loops=1)
   ->  Seq Scan on job_artifact_registry  (cost=0.00..5.80 rows=74 width=796) (actual time=0.035..0.071 rows=80 loops=1)
         Filter: ((last_synced_at >= '2023-06-22 00:00:00-10'::timestamp with time zone) AND (last_synced_at <= '2024-02-03 00:00:00-10'::timestamp with time zone) AND (id >= 1) AND (id <= 10000) AND (state = 2))
 Planning Time: 1.581 ms
 Execution Time: 1.225 ms
(5 rows)

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Michael Kozono

Merge request reports