diff options
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) |