Draft: Disable Orbit beta access on self-managed at GA
What does this MR do and why?
Orbit is generally available, so beta access ends. This depends on !262749 (merged), which makes every enable path on self-managed and Dedicated record direct instead of beta from 19.6. Both should land in 19.6. On self-managed and GitLab Dedicated, this post-deployment migration turns off automatic indexing of top-level groups and turns off Orbit for the groups that were turned on during the beta. The indexer removes their indexed data after a grace period of about 30 days, so an admin or group Owner can turn it on again before then.
The GitLab.com counterpart, !261155 (merged), is already merged.
References
Screenshots or screen recordings
Not applicable, no UI change.
How to set up and validate locally
- Create top-level groups with a
beta, adirectand atrialenrollment, each with an enabled namespace, and setorbit_auto_index_root_namespaceto true inorbit_settings. - Run
bin/rake db:migrate:up VERSION=20261009141237. - Check the migration output for
Removed N Orbit beta enrollments. Check that the setting is false, and only thedirectandtrialgroups still have enrollments and enabled rows (on self-managed and Dedicated, groups turned on before 19.6 havebeta, later ones havedirect).
Spec: bundle exec rspec spec/migrations/20261009141237_disable_orbit_beta_access_on_self_managed_spec.rb (5 examples, passing).
Database review
Statements (the delete runs once per batch of 1,000 enrollments, ordered by the primary key namespace_id):
UPDATE application_settings
SET orbit_settings = jsonb_set(orbit_settings, '{orbit_auto_index_root_namespace}', 'false')
WHERE orbit_settings->>'orbit_auto_index_root_namespace' = 'true';
WITH removed AS (
DELETE FROM orbit_namespace_enrollments
WHERE source = 1 AND namespace_id BETWEEN :min AND :max
RETURNING namespace_id
), removed_enabled AS (
DELETE FROM knowledge_graph_enabled_namespaces
WHERE root_namespace_id IN (SELECT namespace_id FROM removed)
)
SELECT count(*) FROM removed;Plans. The migration is skipped on GitLab.com, so Database Lab has nothing to say; I ran EXPLAIN (ANALYZE, BUFFERS) in GDK on temporary copies of both tables seeded with 200,000 rows (95% beta). Raw output is in the details block below.
- Batch statement (950 rows, with the count): 1.4 ms. Primary key range scan on enrollments, then a unique index lookup on
root_namespace_idper removed row, plus a CTE scan for the count. - Batch range query used by
each_batch_range: 0.2 ms, primary key scan. - Settings update: sequential scan of one row.
Size. Both tables hold one row per enabled top-level group, and automatic indexing enrolls every top-level group. Estimate, not measured on a real instance: a large self-managed instance may have around 10^5 top-level groups, which is the upper bound of rows deleted, about 100 batches and roughly a second of DB time.
Recovery. Deleted rows can't be restored by the migration, because the enrollment source is lost. Orbit is turned on again with the Turn on Orbit for all top-level groups automatically toggle (Admin > Orbit, enrolls every top-level group), or per group by an Owner. Both are expected to cancel the indexer purge during the grace period. The migration logs the number of removed enrollments.
If it fails part-way. The setting update runs first and commits. Each batch is one atomic statement, so no group is left with an enabled row but no enrollment. A re-run only matches what remains. Until it is re-run, some groups keep Orbit on and the rest are off.
Residual risk. The setting is cached in process memory (about 60 seconds), and the hourly auto-index worker may be running while the migration executes. In that window the worker can enroll every top-level group again. These enrollments are direct, so the migration doesn't remove them and the groups stay on. Groups turned on after the 19.6 deploy but before the migration runs are direct too, so the migration keeps them on. The upgrade note tells admins to turn off any group that still shows Orbit on. I did not change the worker here, as it is out of scope. A follow-up could make it re-read the setting without the cache.
MR acceptance checklist
Evaluated against the MR acceptance checklist. The migration is a no-op on GitLab.com. down is a no-op because the deleted rows can't be restored.
Agent context
- Dedicated runs this migration by decision: it is not
Gitlab.com?. - The setting is switched off first. Otherwise the hourly auto-index worker would enroll the groups again (see residual risk above).
- With the prerequisite merge request, every enable path off GitLab.com records
directfrom 19.6. Before that, they all recordedbeta(group settings, GraphQL, admin API, auto-index), so the migration reaches the groups enabled during the beta, not only those from the auto-index setting. - Only
knowledge_graph_enabled_namespacesdeletes reach the indexer. The enrollments table is not replicated, so deleting its rows has no indexer effect. - The indexer's purge grace period is expected to be about 30 days from when the delete replicates, not from when the row was deleted.
- The raw SQL update bypasses the model, so the setting's
after_commithook (which only fires when it turns on) does not run, and the settings cache expires on its normal TTL. db/structure.sqlis unchanged (post-deployment migration);db/schema_migrations/20261009141237is the SHA-256 of the version.- Upgrade note added to
doc/update/versions/gitlab_19_changes.mdunder 19.6. - Not verified: Siphon table coverage on self-managed and Dedicated, and delete-event replication end to end.
EXPLAIN output
BEGIN
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE TABLE
INSERT 0 1
INSERT 0 200000
INSERT 0 200000
ANALYZE
ANALYZE
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=1971.26..1971.27 rows=1 width=8) (actual time=0.405..0.406 rows=1 loops=1)
Buffers: shared hit=3, local hit=1904 read=9 dirtied=8
CTE removed
-> Delete on orbit_namespace_enrollments (cost=0.42..38.45 rows=1035 width=6) (actual time=0.015..0.285 rows=950 loops=1)
Buffers: shared hit=3, local hit=1904 read=9 dirtied=8
-> Index Scan using orbit_namespace_enrollments_pkey on orbit_namespace_enrollments (cost=0.42..38.45 rows=1035 width=6) (actual time=0.011..0.106 rows=950 loops=1)
Index Cond: ((namespace_id >= 1) AND (namespace_id <= 1000))
Filter: (source = 1)
Rows Removed by Filter: 50
Buffers: shared hit=3, local hit=4 read=9
CTE removed_enabled
-> Delete on knowledge_graph_enabled_namespaces (cost=23.71..1909.53 rows=0 width=0) (actual time=0.966..0.966 rows=0 loops=1)
Buffers: local hit=4741 read=9 dirtied=8
-> Nested Loop (cost=23.71..1909.53 rows=1035 width=38) (actual time=0.173..0.848 rows=950 loops=1)
Buffers: local hit=3791 read=9
-> HashAggregate (cost=23.29..33.64 rows=1035 width=40) (actual time=0.167..0.231 rows=950 loops=1)
Group Key: removed_1.namespace_id
Batches: 1 Memory Usage: 193kB
-> CTE Scan on removed removed_1 (cost=0.00..20.70 rows=1035 width=40) (actual time=0.005..0.082 rows=950 loops=1)
-> Index Scan using knowledge_graph_enabled_namespaces_root_namespace_id_idx on knowledge_graph_enabled_namespaces (cost=0.42..1.82 rows=1 width=14) (actual time=0.001..0.001 rows=1 loops=950)
Index Cond: (root_namespace_id = removed_1.namespace_id)
Buffers: local hit=3791 read=9
-> CTE Scan on removed (cost=0.00..20.70 rows=1035 width=0) (actual time=0.016..0.372 rows=950 loops=1)
Buffers: shared hit=3, local hit=1904 read=9 dirtied=8
Planning:
Buffers: shared hit=85 read=1, local hit=6 read=13
Planning Time: 0.295 ms
Execution Time: 1.407 ms
(28 rows)
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=44.95..44.96 rows=1 width=8) (actual time=0.200..0.200 rows=1 loops=1)
Buffers: local hit=13 read=11
-> Limit (cost=0.42..32.45 rows=1000 width=8) (actual time=0.039..0.171 rows=1000 loops=1)
Buffers: local hit=13 read=11
-> Index Scan using orbit_namespace_enrollments_pkey on orbit_namespace_enrollments (cost=0.42..6077.62 rows=189760 width=8) (actual time=0.038..0.121 rows=1000 loops=1)
Index Cond: (namespace_id >= 1)
Filter: (source = 1)
Rows Removed by Filter: 102
Buffers: local hit=13 read=11
Planning:
Buffers: local hit=4
Planning Time: 0.037 ms
Execution Time: 0.206 ms
(13 rows)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Update on application_settings (cost=0.00..28.02 rows=0 width=0) (actual time=0.021..0.021 rows=0 loops=1)
Buffers: local hit=2 read=1 dirtied=1
-> Seq Scan on application_settings (cost=0.00..28.02 rows=6 width=38) (actual time=0.014..0.014 rows=1 loops=1)
Filter: ((orbit_settings ->> 'orbit_auto_index_root_namespace'::text) = 'true'::text)
Buffers: local read=1
Planning:
Buffers: shared hit=16, local read=1
Planning Time: 0.036 ms
Execution Time: 0.026 ms
(9 rows)
ROLLBACK