summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/resource_events/issue_assignment_event.rb14
-rw-r--r--app/models/resource_events/merge_request_assignment_event.rb14
-rw-r--r--app/models/user.rb2
-rw-r--r--db/docs/issue_assignment_events.yml10
-rw-r--r--db/docs/merge_request_assignment_events.yml10
-rw-r--r--db/migrate/20230412073614_create_issue_assignment_events.rb14
-rw-r--r--db/migrate/20230412080242_add_concurrent_fk_to_issue_assignment_events.rb16
-rw-r--r--db/migrate/20230413080906_create_merge_request_assignment_events.rb15
-rw-r--r--db/migrate/20230413080918_add_concurrent_fk_to_merge_request_assignment_events.rb16
-rw-r--r--db/schema_migrations/202304120736141
-rw-r--r--db/schema_migrations/202304120802421
-rw-r--r--db/schema_migrations/202304130809061
-rw-r--r--db/schema_migrations/202304130809181
-rw-r--r--db/structure.sql64
-rw-r--r--doc/administration/postgresql/database_load_balancing.md5
-rw-r--r--doc/development/database/index.md1
-rw-r--r--doc/development/database/load_balancing.md59
-rw-r--r--doc/development/merge_request_concepts/performance.md2
-rw-r--r--doc/development/scalability.md2
-rw-r--r--spec/factories/resource_events/issue_assignment_events.rb9
-rw-r--r--spec/factories/resource_events/merge_request_assignment_events.rb9
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/models/issue_spec.rb1
-rw-r--r--spec/models/merge_request_spec.rb1
-rw-r--r--spec/models/resource_events/issue_assignment_event_spec.rb17
-rw-r--r--spec/models/resource_events/merge_request_assignment_event_spec.rb17
-rw-r--r--spec/models/user_spec.rb2
29 files changed, 306 insertions, 2 deletions
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 77dcaf9336c..8ace5dfff57 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -118,6 +118,7 @@ class Issue < ApplicationRecord
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
has_many :incident_management_timeline_events, class_name: 'IncidentManagement::TimelineEvent', foreign_key: :issue_id, inverse_of: :incident
+ has_many :assignment_events, class_name: 'ResourceEvents::IssueAssignmentEvent', inverse_of: :issue
alias_attribute :escalation_status, :incident_management_issuable_escalation_status
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ab6c58498ad..1e7ff6e8f0e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -122,6 +122,7 @@ class MergeRequest < ApplicationRecord
has_many :reviews, inverse_of: :merge_request
has_many :reviewed_by_users, -> { distinct }, through: :reviews, source: :author
has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request
+ has_many :assignment_events, class_name: 'ResourceEvents::MergeRequestAssignmentEvent', inverse_of: :merge_request
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy,
diff --git a/app/models/resource_events/issue_assignment_event.rb b/app/models/resource_events/issue_assignment_event.rb
new file mode 100644
index 00000000000..b24f181bc48
--- /dev/null
+++ b/app/models/resource_events/issue_assignment_event.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ResourceEvents
+ class IssueAssignmentEvent < ApplicationRecord
+ self.table_name = :issue_assignment_events
+
+ belongs_to :user, optional: true
+ belongs_to :issue
+
+ validates :issue, presence: true
+
+ enum action: { add: 1, remove: 2 }
+ end
+end
diff --git a/app/models/resource_events/merge_request_assignment_event.rb b/app/models/resource_events/merge_request_assignment_event.rb
new file mode 100644
index 00000000000..898594b7008
--- /dev/null
+++ b/app/models/resource_events/merge_request_assignment_event.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ResourceEvents
+ class MergeRequestAssignmentEvent < ApplicationRecord
+ self.table_name = :merge_request_assignment_events
+
+ belongs_to :user, optional: true
+ belongs_to :merge_request
+
+ validates :merge_request, presence: true
+
+ enum action: { add: 1, remove: 2 }
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 16b4e604a9e..52c80a7e579 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -266,6 +266,8 @@ class User < ApplicationRecord
has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+ has_many :issue_assignment_events, class_name: 'ResourceEvents::IssueAssignmentEvent', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+ has_many :merge_request_assignment_events, class_name: 'ResourceEvents::MergeRequestAssignmentEvent', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail'
has_many :user_achievements, class_name: 'Achievements::UserAchievement', inverse_of: :user
diff --git a/db/docs/issue_assignment_events.yml b/db/docs/issue_assignment_events.yml
new file mode 100644
index 00000000000..0ba9f9ca21f
--- /dev/null
+++ b/db/docs/issue_assignment_events.yml
@@ -0,0 +1,10 @@
+---
+table_name: issue_assignment_events
+classes:
+- ResourceEvents::IssueAssignmentEvent
+feature_categories:
+- value_stream_management
+description: Tracks the assignment and unassignment events for issues
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117379
+milestone: '15.11'
+gitlab_schema: gitlab_main
diff --git a/db/docs/merge_request_assignment_events.yml b/db/docs/merge_request_assignment_events.yml
new file mode 100644
index 00000000000..49eeefcbcf0
--- /dev/null
+++ b/db/docs/merge_request_assignment_events.yml
@@ -0,0 +1,10 @@
+---
+table_name: merge_request_assignment_events
+classes:
+- ResourceEvents::MergeRequestAssignmentEvent
+feature_categories:
+- value_stream_management
+description: Tracks the assignment and unassignment events for merge requests
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117379
+milestone: '15.11'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230412073614_create_issue_assignment_events.rb b/db/migrate/20230412073614_create_issue_assignment_events.rb
new file mode 100644
index 00000000000..1b57c59eb2b
--- /dev/null
+++ b/db/migrate/20230412073614_create_issue_assignment_events.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateIssueAssignmentEvents < Gitlab::Database::Migration[2.1]
+ def change
+ create_table :issue_assignment_events do |t|
+ t.references :user, null: true, index: true, foreign_key: { on_delete: :nullify }
+ t.bigint :issue_id, null: false
+ t.datetime_with_timezone :created_at, null: false, default: -> { 'NOW()' }
+ t.integer :action, limit: 2, null: false, default: 1
+
+ t.index %i[issue_id action created_at id], name: 'index_on_issue_assignment_events_issue_id_action_created_at_id'
+ end
+ end
+end
diff --git a/db/migrate/20230412080242_add_concurrent_fk_to_issue_assignment_events.rb b/db/migrate/20230412080242_add_concurrent_fk_to_issue_assignment_events.rb
new file mode 100644
index 00000000000..3bff98c6e69
--- /dev/null
+++ b/db/migrate/20230412080242_add_concurrent_fk_to_issue_assignment_events.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddConcurrentFkToIssueAssignmentEvents < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :issue_assignment_events,
+ :issues,
+ column: :issue_id,
+ on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key_if_exists :resource_assignment_events, column: :issue_id
+ end
+end
diff --git a/db/migrate/20230413080906_create_merge_request_assignment_events.rb b/db/migrate/20230413080906_create_merge_request_assignment_events.rb
new file mode 100644
index 00000000000..b976dc8935a
--- /dev/null
+++ b/db/migrate/20230413080906_create_merge_request_assignment_events.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateMergeRequestAssignmentEvents < Gitlab::Database::Migration[2.1]
+ def change
+ create_table :merge_request_assignment_events do |t|
+ t.references :user, null: true, index: true, foreign_key: { on_delete: :nullify }
+ t.bigint :merge_request_id, null: false
+ t.datetime_with_timezone :created_at, null: false, default: -> { 'NOW()' }
+ t.integer :action, limit: 2, null: false, default: 1
+
+ t.index %i[merge_request_id action created_at id],
+ name: 'index_on_mr_assignment_events_mr_id_action_created_at_id'
+ end
+ end
+end
diff --git a/db/migrate/20230413080918_add_concurrent_fk_to_merge_request_assignment_events.rb b/db/migrate/20230413080918_add_concurrent_fk_to_merge_request_assignment_events.rb
new file mode 100644
index 00000000000..5b7d08a3547
--- /dev/null
+++ b/db/migrate/20230413080918_add_concurrent_fk_to_merge_request_assignment_events.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddConcurrentFkToMergeRequestAssignmentEvents < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :merge_request_assignment_events,
+ :merge_requests,
+ column: :merge_request_id,
+ on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key_if_exists :merge_request_assignment_events, column: :merge_request_id
+ end
+end
diff --git a/db/schema_migrations/20230412073614 b/db/schema_migrations/20230412073614
new file mode 100644
index 00000000000..bb6ac60b051
--- /dev/null
+++ b/db/schema_migrations/20230412073614
@@ -0,0 +1 @@
+984ebbfc7a8f6ba62715da2fe5ff46ab4030eb17baff69e82f56d1596c6f2e31 \ No newline at end of file
diff --git a/db/schema_migrations/20230412080242 b/db/schema_migrations/20230412080242
new file mode 100644
index 00000000000..a19a1b31b40
--- /dev/null
+++ b/db/schema_migrations/20230412080242
@@ -0,0 +1 @@
+fabf026dac1c69b291895dcc047bb03baf31376f72d289d798d537da1b4ac53a \ No newline at end of file
diff --git a/db/schema_migrations/20230413080906 b/db/schema_migrations/20230413080906
new file mode 100644
index 00000000000..20037d0f314
--- /dev/null
+++ b/db/schema_migrations/20230413080906
@@ -0,0 +1 @@
+44c6a5d0a7e3083dd5bf0afcfeff9cbd1061a3bb444504d11c44c38adeb75123 \ No newline at end of file
diff --git a/db/schema_migrations/20230413080918 b/db/schema_migrations/20230413080918
new file mode 100644
index 00000000000..866d0a817bc
--- /dev/null
+++ b/db/schema_migrations/20230413080918
@@ -0,0 +1 @@
+38e2f3cf25cc09d9f396de1fa0d299bde55daeb59c98d886df02db1d337a452f \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7eb0f3e75b8..9e6b0dc0561 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17296,6 +17296,23 @@ CREATE TABLE issue_assignees (
issue_id integer NOT NULL
);
+CREATE TABLE issue_assignment_events (
+ id bigint NOT NULL,
+ user_id bigint,
+ issue_id bigint NOT NULL,
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
+ action smallint DEFAULT 1 NOT NULL
+);
+
+CREATE SEQUENCE issue_assignment_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE issue_assignment_events_id_seq OWNED BY issue_assignment_events.id;
+
CREATE TABLE issue_customer_relations_contacts (
id bigint NOT NULL,
issue_id bigint NOT NULL,
@@ -17963,6 +17980,23 @@ CREATE SEQUENCE merge_request_assignees_id_seq
ALTER SEQUENCE merge_request_assignees_id_seq OWNED BY merge_request_assignees.id;
+CREATE TABLE merge_request_assignment_events (
+ id bigint NOT NULL,
+ user_id bigint,
+ merge_request_id bigint NOT NULL,
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
+ action smallint DEFAULT 1 NOT NULL
+);
+
+CREATE SEQUENCE merge_request_assignment_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE merge_request_assignment_events_id_seq OWNED BY merge_request_assignment_events.id;
+
CREATE TABLE merge_request_blocks (
id bigint NOT NULL,
blocking_merge_request_id integer NOT NULL,
@@ -25083,6 +25117,8 @@ ALTER TABLE ONLY issuable_severities ALTER COLUMN id SET DEFAULT nextval('issuab
ALTER TABLE ONLY issuable_slas ALTER COLUMN id SET DEFAULT nextval('issuable_slas_id_seq'::regclass);
+ALTER TABLE ONLY issue_assignment_events ALTER COLUMN id SET DEFAULT nextval('issue_assignment_events_id_seq'::regclass);
+
ALTER TABLE ONLY issue_customer_relations_contacts ALTER COLUMN id SET DEFAULT nextval('issue_customer_relations_contacts_id_seq'::regclass);
ALTER TABLE ONLY issue_email_participants ALTER COLUMN id SET DEFAULT nextval('issue_email_participants_id_seq'::regclass);
@@ -25143,6 +25179,8 @@ ALTER TABLE ONLY members ALTER COLUMN id SET DEFAULT nextval('members_id_seq'::r
ALTER TABLE ONLY merge_request_assignees ALTER COLUMN id SET DEFAULT nextval('merge_request_assignees_id_seq'::regclass);
+ALTER TABLE ONLY merge_request_assignment_events ALTER COLUMN id SET DEFAULT nextval('merge_request_assignment_events_id_seq'::regclass);
+
ALTER TABLE ONLY merge_request_blocks ALTER COLUMN id SET DEFAULT nextval('merge_request_blocks_id_seq'::regclass);
ALTER TABLE ONLY merge_request_cleanup_schedules ALTER COLUMN merge_request_id SET DEFAULT nextval('merge_request_cleanup_schedules_merge_request_id_seq'::regclass);
@@ -27174,6 +27212,9 @@ ALTER TABLE ONLY issuable_slas
ALTER TABLE ONLY issue_assignees
ADD CONSTRAINT issue_assignees_pkey PRIMARY KEY (issue_id, user_id);
+ALTER TABLE ONLY issue_assignment_events
+ ADD CONSTRAINT issue_assignment_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY issue_customer_relations_contacts
ADD CONSTRAINT issue_customer_relations_contacts_pkey PRIMARY KEY (id);
@@ -27273,6 +27314,9 @@ ALTER TABLE ONLY members
ALTER TABLE ONLY merge_request_assignees
ADD CONSTRAINT merge_request_assignees_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY merge_request_assignment_events
+ ADD CONSTRAINT merge_request_assignment_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY merge_request_blocks
ADD CONSTRAINT merge_request_blocks_pkey PRIMARY KEY (id);
@@ -30850,6 +30894,8 @@ CREATE UNIQUE INDEX index_issuable_slas_on_issue_id ON issuable_slas USING btree
CREATE INDEX index_issue_assignees_on_user_id_and_issue_id ON issue_assignees USING btree (user_id, issue_id);
+CREATE INDEX index_issue_assignment_events_on_user_id ON issue_assignment_events USING btree (user_id);
+
CREATE UNIQUE INDEX index_issue_crm_contacts_on_issue_id_and_contact_id ON issue_customer_relations_contacts USING btree (issue_id, contact_id);
CREATE INDEX index_issue_customer_relations_contacts_on_contact_id ON issue_customer_relations_contacts USING btree (contact_id);
@@ -31082,6 +31128,8 @@ CREATE UNIQUE INDEX index_merge_request_assignees_on_merge_request_id_and_user_i
CREATE INDEX index_merge_request_assignees_on_user_id ON merge_request_assignees USING btree (user_id);
+CREATE INDEX index_merge_request_assignment_events_on_user_id ON merge_request_assignment_events USING btree (user_id);
+
CREATE INDEX index_merge_request_blocks_on_blocked_merge_request_id ON merge_request_blocks USING btree (blocked_merge_request_id);
CREATE UNIQUE INDEX index_merge_request_cleanup_schedules_on_merge_request_id ON merge_request_cleanup_schedules USING btree (merge_request_id);
@@ -31404,6 +31452,8 @@ CREATE INDEX index_on_identities_lower_extern_uid_and_provider ON identities USI
CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON analytics_usage_trends_measurements USING btree (identifier, recorded_at);
+CREATE INDEX index_on_issue_assignment_events_issue_id_action_created_at_id ON issue_assignment_events USING btree (issue_id, action, created_at, id);
+
CREATE INDEX index_on_issues_closed_incidents_by_project_id_and_closed_at ON issues USING btree (project_id, closed_at) WHERE ((issue_type = 1) AND (state_id = 2));
CREATE INDEX index_on_issues_health_status_asc_order ON issues USING btree (project_id, health_status, id DESC, state_id, issue_type);
@@ -31418,6 +31468,8 @@ CREATE INDEX index_on_merge_requests_for_latest_diffs ON merge_requests USING bt
COMMENT ON INDEX index_on_merge_requests_for_latest_diffs IS 'Index used to efficiently obtain the oldest merge request for a commit SHA';
+CREATE INDEX index_on_mr_assignment_events_mr_id_action_created_at_id ON merge_request_assignment_events USING btree (merge_request_id, action, created_at, id);
+
CREATE INDEX index_on_namespaces_lower_name ON namespaces USING btree (lower((name)::text));
CREATE INDEX index_on_namespaces_lower_path ON namespaces USING btree (lower((path)::text));
@@ -34398,6 +34450,9 @@ ALTER TABLE ONLY merge_requests
ALTER TABLE ONLY user_interacted_projects
ADD CONSTRAINT fk_0894651f08 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY merge_request_assignment_events
+ ADD CONSTRAINT fk_08f7602bfd FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY dast_sites
ADD CONSTRAINT fk_0a57f2271b FOREIGN KEY (dast_site_validation_id) REFERENCES dast_site_validations(id) ON DELETE SET NULL;
@@ -35160,6 +35215,9 @@ ALTER TABLE ONLY todos
ALTER TABLE ONLY dast_site_profiles_pipelines
ADD CONSTRAINT fk_cf05cf8fe1 FOREIGN KEY (dast_site_profile_id) REFERENCES dast_site_profiles(id) ON DELETE CASCADE;
+ALTER TABLE ONLY issue_assignment_events
+ ADD CONSTRAINT fk_cfd2073177 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_cff7185ad2 FOREIGN KEY (reset_checksum_event_id) REFERENCES geo_reset_checksum_events(id) ON DELETE CASCADE;
@@ -35412,6 +35470,9 @@ ALTER TABLE ONLY terraform_state_versions
ALTER TABLE ONLY search_namespace_index_assignments
ADD CONSTRAINT fk_rails_06f9b905d3 FOREIGN KEY (namespace_id) REFERENCES namespaces(id);
+ALTER TABLE ONLY issue_assignment_events
+ ADD CONSTRAINT fk_rails_07683f8e80 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY work_item_hierarchy_restrictions
ADD CONSTRAINT fk_rails_08cd7fef58 FOREIGN KEY (child_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
@@ -35835,6 +35896,9 @@ ALTER TABLE ONLY batched_background_migration_jobs
ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_43241e8d29 FOREIGN KEY (strategy_id) REFERENCES operations_strategies(id) ON DELETE CASCADE;
+ALTER TABLE ONLY merge_request_assignment_events
+ ADD CONSTRAINT fk_rails_4378a2e8d7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY lfs_file_locks
ADD CONSTRAINT fk_rails_43df7a0412 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/administration/postgresql/database_load_balancing.md b/doc/administration/postgresql/database_load_balancing.md
index 15129770888..d5cf93a135a 100644
--- a/doc/administration/postgresql/database_load_balancing.md
+++ b/doc/administration/postgresql/database_load_balancing.md
@@ -235,3 +235,8 @@ operation retries up to 3 times using an exponential back-off.
When using load balancing, you should be able to safely restart a database server
without it immediately leading to errors being presented to the users.
+
+### Development guide
+
+For detailed development guide on database load balancing,
+see [the development documentation](../../development/database/load_balancing.md).
diff --git a/doc/development/database/index.md b/doc/development/database/index.md
index 2cb8509e203..f532e054849 100644
--- a/doc/development/database/index.md
+++ b/doc/development/database/index.md
@@ -64,6 +64,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
- [Hash indexes](hash_indexes.md)
- [Insert into tables in batches](insert_into_tables_in_batches.md)
- [Iterating tables in batches](iterating_tables_in_batches.md)
+- [Load balancing](load_balancing.md)
- [`NOT NULL` constraints](not_null_constraints.md)
- [Ordering table columns](ordering_table_columns.md)
- [Pagination guidelines](pagination_guidelines.md)
diff --git a/doc/development/database/load_balancing.md b/doc/development/database/load_balancing.md
new file mode 100644
index 00000000000..f623ad1eab0
--- /dev/null
+++ b/doc/development/database/load_balancing.md
@@ -0,0 +1,59 @@
+---
+stage: Data Stores
+group: Database
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Database load balancing
+
+With database load balancing, read-only queries can be distributed across multiple
+PostgreSQL nodes to increase performance.
+
+This documentation provides a technical overview on how database load balancing
+is implemented in GitLab Rails and Sidekiq.
+
+## Nomenclature
+
+1. **Host**: Each database host. It could be a primary or a replica.
+1. **Primary**: Primary PostgreSQL host that is used for write-only and read-and-write operations.
+1. **Replica**: Secondary PostgreSQL hosts that are used for read-only operations.
+1. **Workload**: a Rails request or a Sidekiq job that requires database connections.
+
+## Components
+
+F few Ruby classes are involved in the load balancing process. All of them are
+in the namespace `Gitlab::Database::LoadBalancing`:
+
+1. `Host`
+1. `LoadBalancer`
+1. `ConnectionProxy`
+1. `Session`
+
+Each workload begins with a new instance of `Gitlab::Database::LoadBalancing::Session`.
+The `Session` keeps track of the database operations that have been performed. It then
+determines if the workload requires a connection to either the primary host or a replica host.
+
+When the workload requires a database connection through `ActiveRecord`,
+`ConnectionProxy` first redirects the connection request to `LoadBalancer`.
+`ConnectionProxy` requests either a `read` or `read_write` connection from the `LoadBalancer`
+depending on a few criteria:
+
+1. Whether the query is a read-only or it requires write.
+1. Whether the `Session` has recorded a write operation previously.
+1. Whether any special blocks have been used to prefer primary or replica, such as:
+ - `use_primary`
+ - `ignore_writes`
+ - `use_replicas_for_read_queries`
+ - `fallback_to_replicas_for_ambiguous_queries`
+
+`LoadBalancer` then yields the requested connection from the respective database connection pool.
+It yields either:
+
+- A `read_write` connection from the primary's connection pool.
+- A `read` connection from the replicas' connection pools.
+
+When responding to a request for a `read` connection, `LoadBalancer` would
+first attempt to load balance the connection across the replica hosts.
+It looks for the next `online` replica host and yields a connection from the host's connection pool.
+A replica host is considered `online` if it is up-to-date with the primary, based on
+either the replication lag size or time. The thresholds for these requirements are configurable.
diff --git a/doc/development/merge_request_concepts/performance.md b/doc/development/merge_request_concepts/performance.md
index 3b2a097ea2d..174307fc6a7 100644
--- a/doc/development/merge_request_concepts/performance.md
+++ b/doc/development/merge_request_concepts/performance.md
@@ -158,7 +158,7 @@ query. This in turn makes it much harder for this code to overload a database.
## Use read replicas when possible
-In a DB cluster we have many read replicas and one primary. A classic use of scaling the DB is to have read-only actions be performed by the replicas. We use [load balancing](../../administration/postgresql/database_load_balancing.md) to distribute this load. This allows for the replicas to grow as the pressure on the DB grows.
+In a DB cluster we have many read replicas and one primary. A classic use of scaling the DB is to have read-only actions be performed by the replicas. We use [load balancing](../database/load_balancing.md) to distribute this load. This allows for the replicas to grow as the pressure on the DB grows.
By default, queries use read-only replicas, but due to
[primary sticking](../../administration/postgresql/database_load_balancing.md#primary-sticking), GitLab uses the
diff --git a/doc/development/scalability.md b/doc/development/scalability.md
index de9c57c2f2a..733e94cb5a7 100644
--- a/doc/development/scalability.md
+++ b/doc/development/scalability.md
@@ -123,7 +123,7 @@ the read replicas. [Omnibus ships with Patroni](../administration/postgresql/rep
#### Load-balancing
-GitLab EE has [application support for load balancing using read replicas](../administration/postgresql/database_load_balancing.md). This load balancer does
+GitLab EE has [application support for load balancing using read replicas](database/load_balancing.md). This load balancer does
some actions that aren't traditionally available in standard load balancers. For
example, the application considers a replica only if its replication lag is low
(for example, WAL data behind by less than 100 MB).
diff --git a/spec/factories/resource_events/issue_assignment_events.rb b/spec/factories/resource_events/issue_assignment_events.rb
new file mode 100644
index 00000000000..72319905d0d
--- /dev/null
+++ b/spec/factories/resource_events/issue_assignment_events.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :issue_assignment_event, class: 'ResourceEvents::IssueAssignmentEvent' do
+ action { :add }
+ issue
+ user
+ end
+end
diff --git a/spec/factories/resource_events/merge_request_assignment_events.rb b/spec/factories/resource_events/merge_request_assignment_events.rb
new file mode 100644
index 00000000000..6d388543648
--- /dev/null
+++ b/spec/factories/resource_events/merge_request_assignment_events.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_assignment_event, class: 'ResourceEvents::MergeRequestAssignmentEvent' do
+ action { :add }
+ merge_request
+ user
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 74ac0639eb6..66b57deb643 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -14,6 +14,7 @@ issues:
- resource_milestone_events
- resource_state_events
- resource_iteration_events
+- assignment_events
- sent_notifications
- sentry_issue
- issuable_severity
@@ -180,6 +181,7 @@ merge_requests:
- resource_milestone_events
- resource_state_events
- resource_iteration_events
+- assignment_events
- label_links
- labels
- last_edited_by
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index d238133b59e..38f50f7627e 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe Issue, feature_category: :team_planning do
it { is_expected.to have_many(:issue_customer_relations_contacts) }
it { is_expected.to have_many(:customer_relations_contacts) }
it { is_expected.to have_many(:incident_management_timeline_events) }
+ it { is_expected.to have_many(:assignment_events).class_name('ResourceEvents::IssueAssignmentEvent').inverse_of(:issue) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f0b8eb985c4..5169fdf504d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to have_many(:reviewed_by_users).through(:reviews).source(:author) }
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
it { is_expected.to have_many(:created_environments).class_name('Environment').inverse_of(:merge_request) }
+ it { is_expected.to have_many(:assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent').inverse_of(:merge_request) }
context 'for forks' do
let!(:project) { create(:project) }
diff --git a/spec/models/resource_events/issue_assignment_event_spec.rb b/spec/models/resource_events/issue_assignment_event_spec.rb
new file mode 100644
index 00000000000..bc217da2812
--- /dev/null
+++ b/spec/models/resource_events/issue_assignment_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::IssueAssignmentEvent, feature_category: :value_stream_management, type: :model do
+ subject(:event) { build(:issue_assignment_event) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:issue) }
+ end
+end
diff --git a/spec/models/resource_events/merge_request_assignment_event_spec.rb b/spec/models/resource_events/merge_request_assignment_event_spec.rb
new file mode 100644
index 00000000000..15f4c088333
--- /dev/null
+++ b/spec/models/resource_events/merge_request_assignment_event_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::MergeRequestAssignmentEvent, feature_category: :value_stream_management, type: :model do
+ subject(:event) { build(:merge_request_assignment_event) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:merge_request) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:merge_request) }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 93bf53a89cc..9a09181e5ec 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -182,6 +182,8 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') }
it { is_expected.to have_many(:audit_events).with_foreign_key(:author_id).inverse_of(:user) }
it { is_expected.to have_many(:abuse_trust_scores).class_name('Abuse::TrustScore') }
+ it { is_expected.to have_many(:issue_assignment_events).class_name('ResourceEvents::IssueAssignmentEvent') }
+ it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
it do
is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee)