Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-20 03:08:21 +00:00
parent 48641d6d58
commit 93c966ae2a
25 changed files with 411 additions and 392 deletions

View file

@ -173,8 +173,9 @@ export default {
addList: ({ commit, dispatch, getters }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
dispatch('fetchItemsForList', {
listId: getters.getListByTitle(ListTypeTitles.backlog).id,
listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
},
@ -294,7 +295,7 @@ export default {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
} else {
dispatch('fetchItemsForList', {
listId: getters.getListByTitle(ListTypeTitles.backlog).id,
listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
}
},
@ -305,6 +306,8 @@ export default {
},
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
if (!listId) return null;
if (!fetchNext) {
commit(types.RESET_ITEMS_FOR_LIST, listId);
}

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::CohortsController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
include RedisTracking
feature_category :devops_reports
@ -21,6 +21,6 @@ class Admin::CohortsController < Admin::ApplicationController
end
def track_cohorts_visit
track_visit('i_analytics_cohorts') if trackable_html_request?
track_unique_redis_hll_event('i_analytics_cohorts') if trackable_html_request?
end
end

View file

@ -1,9 +1,9 @@
# frozen_string_literal: true
class Admin::UsageTrendsController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
include RedisTracking
track_unique_visits :index, target_id: 'i_analytics_instance_statistics'
track_redis_hll_event :index, name: 'i_analytics_instance_statistics'
feature_category :devops_reports

View file

@ -3,7 +3,6 @@
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
include PaginatedCollection
include Analytics::UniqueVisitsHelper
before_action :authorize_read_project!, only: :index
before_action :authorize_read_group!, only: :index

View file

@ -4,12 +4,12 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
include Analytics::UniqueVisitsHelper
include GracefulTimeoutHandling
include RedisTracking
before_action :authorize_read_cycle_analytics!
track_unique_visits :show, target_id: 'p_analytics_valuestream'
track_redis_hll_event :show, name: 'p_analytics_valuestream'
feature_category :planning_analytics

View file

@ -2,14 +2,14 @@
class Projects::GraphsController < Projects::ApplicationController
include ExtractsPath
include Analytics::UniqueVisitsHelper
include RedisTracking
# Authorize
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_read_repository_graphs!
track_unique_visits :charts, target_id: 'p_analytics_repo'
track_redis_hll_event :charts, name: 'p_analytics_repo'
feature_category :source_code_management

View file

@ -2,7 +2,7 @@
class Projects::PipelinesController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
include Analytics::UniqueVisitsHelper
include RedisTracking
before_action :disable_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts, :config_variables]
@ -24,7 +24,7 @@ class Projects::PipelinesController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
track_unique_visits :charts, target_id: 'p_analytics_pipelines'
track_redis_hll_event :charts, name: 'p_analytics_pipelines'
wrap_parameters Ci::Pipeline

View file

@ -1,31 +0,0 @@
# frozen_string_literal: true
module Analytics
module UniqueVisitsHelper
include Gitlab::Tracking::Helpers
extend ActiveSupport::Concern
def visitor_id
return cookies[:visitor_id] if cookies[:visitor_id].present?
return unless current_user
uuid = SecureRandom.uuid
cookies[:visitor_id] = { value: uuid, expires: 24.months }
uuid
end
def track_visit(target_id)
return unless visitor_id
Gitlab::Analytics::UniqueVisits.new.track_visit(target_id, values: visitor_id)
end
class_methods do
def track_unique_visits(controller_actions, target_id:)
after_action only: controller_actions, if: -> { trackable_html_request? } do
track_visit(target_id)
end
end
end
end
end

View file

@ -159,9 +159,8 @@ module AtomicInternalId
# Defines class methods:
#
# - with_{scope}_{column}_supply
# This method can be used to allocate a block of IID values during
# bulk operations (importing/copying, etc). This can be more efficient
# than creating instances one-by-one.
# This method can be used to allocate a stream of IID values during
# bulk operations (importing/copying, etc).
#
# Pass in a block that receives a `Supply` instance. To allocate a new
# IID value, call `Supply#next_value`.
@ -181,14 +180,8 @@ module AtomicInternalId
scope_attrs = ::AtomicInternalId.scope_attrs(scope_value)
usage = ::AtomicInternalId.scope_usage(self)
generator = InternalId::InternalIdGenerator.new(subject, scope_attrs, usage, init)
generator.with_lock do
supply = Supply.new(generator.record.last_value)
block.call(supply)
ensure
generator.track_greatest(supply.current_value) if supply
end
supply = Supply.new(-> { InternalId.generate_next(subject, scope_attrs, usage, init) })
block.call(supply)
end
end
end
@ -236,14 +229,14 @@ module AtomicInternalId
end
class Supply
attr_reader :current_value
attr_reader :generator
def initialize(start_value)
@current_value = start_value
def initialize(generator)
@generator = generator
end
def next_value
@current_value += 1
@generator.call
end
end
end

View file

@ -16,7 +16,7 @@
# * Add `usage` value to enum
# * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ApplicationRecord
include Gitlab::Utils::StrongMemoize
extend Gitlab::Utils::StrongMemoize
belongs_to :project
belongs_to :namespace
@ -25,6 +25,10 @@ class InternalId < ApplicationRecord
validates :usage, presence: true
scope :filter_by, -> (scope, usage) do
where(**scope, usage: usage)
end
# Increments #last_value and saves the record
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
@ -53,18 +57,15 @@ class InternalId < ApplicationRecord
class << self
def track_greatest(subject, scope, usage, new_value, init)
InternalIdGenerator.new(subject, scope, usage, init)
.track_greatest(new_value)
build_generator(subject, scope, usage, init).track_greatest(new_value)
end
def generate_next(subject, scope, usage, init)
InternalIdGenerator.new(subject, scope, usage, init)
.generate
build_generator(subject, scope, usage, init).generate
end
def reset(subject, scope, usage, value)
InternalIdGenerator.new(subject, scope, usage)
.reset(value)
build_generator(subject, scope, usage).reset(value)
end
# Flushing records is generally safe in a sense that those
@ -77,11 +78,36 @@ class InternalId < ApplicationRecord
where(filter).delete_all
end
def internal_id_transactions_increment(operation:, usage:)
self.internal_id_transactions_total.increment(
operation: operation,
usage: usage.to_s,
in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s
)
end
def internal_id_transactions_total
strong_memoize(:internal_id_transactions_total) do
name = :gitlab_internal_id_transactions_total
comment = 'Counts all the internal ids happening within transaction'
Gitlab::Metrics.counter(name, comment)
end
end
private
def build_generator(subject, scope, usage, init = nil)
if Feature.enabled?(:generate_iids_without_explicit_locking)
ImplicitlyLockingInternalIdGenerator.new(subject, scope, usage, init)
else
InternalIdGenerator.new(subject, scope, usage, init)
end
end
end
class InternalIdGenerator
extend Gitlab::Utils::StrongMemoize
# Generate next internal id for a given scope and usage.
#
# For currently supported usages, see #usage enum.
@ -117,7 +143,7 @@ class InternalId < ApplicationRecord
# init: Block that gets called to initialize InternalId record if not present
# Make sure to not throw exceptions in the absence of records (if this is expected).
def generate
self.class.internal_id_transactions_increment(operation: :generate, usage: usage)
InternalId.internal_id_transactions_increment(operation: :generate, usage: usage)
subject.transaction do
# Create a record in internal_ids if one does not yet exist
@ -134,7 +160,7 @@ class InternalId < ApplicationRecord
def reset(value)
return false unless value
self.class.internal_id_transactions_increment(operation: :reset, usage: usage)
InternalId.internal_id_transactions_increment(operation: :reset, usage: usage)
updated =
InternalId
@ -149,8 +175,9 @@ class InternalId < ApplicationRecord
# and set its new_value if it is higher than the current last_value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
self.class.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
subject.transaction do
record.track_greatest_and_save!(new_value)
@ -162,7 +189,7 @@ class InternalId < ApplicationRecord
end
def with_lock(&block)
self.class.internal_id_transactions_increment(operation: :with_lock, usage: usage)
InternalId.internal_id_transactions_increment(operation: :with_lock, usage: usage)
record.with_lock(&block)
end
@ -199,22 +226,118 @@ class InternalId < ApplicationRecord
rescue ActiveRecord::RecordNotUnique
lookup
end
end
def self.internal_id_transactions_increment(operation:, usage:)
self.internal_id_transactions_total.increment(
operation: operation,
usage: usage.to_s,
in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s
)
class ImplicitlyLockingInternalIdGenerator
# Generate next internal id for a given scope and usage.
#
# For currently supported usages, see #usage enum.
#
# The method implements a locking scheme that has the following properties:
# 1) Generated sequence of internal ids is unique per (scope and usage)
# 2) The method is thread-safe and may be used in concurrent threads/processes.
# 3) The generated sequence is gapless.
# 4) In the absence of a record in the internal_ids table, one will be created
# and last_value will be calculated on the fly.
#
# subject: The instance or class we're generating an internal id for.
# scope: Attributes that define the scope for id generation.
# Valid keys are `project/project_id` and `namespace/namespace_id`.
# usage: Symbol to define the usage of the internal id, see InternalId.usages
# init: Proc that accepts the subject and the scope and returns Integer|NilClass
attr_reader :subject, :scope, :scope_attrs, :usage, :init
def initialize(subject, scope, usage, init = nil)
@subject = subject
@scope = scope
@usage = usage
@init = init
raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
unless InternalId.usages.has_key?(usage.to_s)
raise ArgumentError, "Usage '#{usage}' is unknown. Supported values are #{InternalId.usages.keys} from InternalId.usages"
end
end
def self.internal_id_transactions_total
strong_memoize(:internal_id_transactions_total) do
name = :gitlab_internal_id_transactions_total
comment = 'Counts all the internal ids happening within transaction'
# Generates next internal id and returns it
# init: Block that gets called to initialize InternalId record if not present
# Make sure to not throw exceptions in the absence of records (if this is expected).
def generate
InternalId.internal_id_transactions_increment(operation: :generate, usage: usage)
Gitlab::Metrics.counter(name, comment)
next_iid = update_record!(subject, scope, usage, arel_table[:last_value] + 1)
return next_iid if next_iid
create_record!(subject, scope, usage, init) do |iid|
iid.last_value += 1
end
rescue ActiveRecord::RecordNotUnique
retry
end
# Reset tries to rewind to `value-1`. This will only succeed,
# if `value` stored in database is equal to `last_value`.
# value: The expected last_value to decrement
def reset(value)
return false unless value
InternalId.internal_id_transactions_increment(operation: :reset, usage: usage)
iid = update_record!(subject, scope.merge(last_value: value), usage, arel_table[:last_value] - 1)
iid == value - 1
end
# Create a record in internal_ids if one does not yet exist
# and set its new_value if it is higher than the current last_value
def track_greatest(new_value)
InternalId.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
function = Arel::Nodes::NamedFunction.new('GREATEST', [
arel_table[:last_value],
new_value.to_i
])
next_iid = update_record!(subject, scope, usage, function)
return next_iid if next_iid
create_record!(subject, scope, usage, init) do |object|
object.last_value = [object.last_value, new_value].max
end
rescue ActiveRecord::RecordNotUnique
retry
end
private
def update_record!(subject, scope, usage, new_value)
stmt = Arel::UpdateManager.new
stmt.table(arel_table)
stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value')
end
def create_record!(subject, scope, usage, init)
raise ArgumentError, 'Cannot initialize without init!' unless init
instance = subject.is_a?(::Class) ? nil : subject
subject.transaction(requires_new: true) do
last_value = init.call(instance, scope) || 0
internal_id = InternalId.create!(**scope, usage: usage, last_value: last_value) do |subject|
yield subject if block_given?
end
internal_id.last_value
end
end
def arel_table
InternalId.arel_table
end
end
end

View file

@ -0,0 +1,8 @@
---
name: generate_iids_without_explicit_locking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65590
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335431
milestone: '14.2'
type: development
group: group::database
default_enabled: false

View file

@ -10,6 +10,7 @@ Configure your organization and its users. Determine user roles
and give everyone access to the projects they need.
- [Members](../user/project/members/index.md)
- [Workspace](../user/workspace/index.md) _(Coming soon)_
- [Groups](../user/group/index.md)
- [User account options](../user/profile/index.md)
- [SSH keys](../ssh/index.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

View file

@ -0,0 +1,34 @@
---
stage: Manage
group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Workspace
Workspace will be the top-level [namespace](../group/index.md#namespaces) for you to manage
everything GitLab, including:
- Defining and applying settings to all of your groups, subgroups, and projects.
- Aggregating data from all your groups, subgroups, and projects.
Workspace will take many of the features from the
[Admin Area](../admin_area/index.md), and there will be one workspace per:
- Instance, for self-managed instances.
- Namespace, for GitLab.com.
NOTE:
Workspace is currently in development.
## Concept previews
The following provide a preview to the Workspace concept.
![Workspace Overview](img/1.1-Instance_overview.png)
![Groups Overview](img/1.2-Groups_overview.png)
![Admin Overview](img/1.3-Admin.png)
![Admin Overview](img/Admin_Settings.png)

View file

@ -3,10 +3,6 @@
module Gitlab
module Analytics
class UniqueVisits
def track_visit(*args, **kwargs)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(*args, **kwargs)
end
# Returns number of unique visitors for given targets in given time frame
#
# @param [String, Array[<String>]] targets ids of targets to count visits on. Special case for :any

View file

@ -291,7 +291,7 @@ export const setMockEndpoints = (opts = {}) => {
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Backlog',
title: 'Open',
position: -Infinity,
listType: 'backlog',
collapsed: false,

View file

@ -715,6 +715,19 @@ describe('fetchItemsForList', () => {
[listId]: pageInfo,
};
describe('when list id is undefined', () => {
it('does not call the query', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await actions.fetchItemsForList(
{ state, getters: () => {}, commit: () => {} },
{ listId: undefined },
);
expect(gqlClient.query).toHaveBeenCalledTimes(0);
});
});
it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', (done) => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);

View file

@ -1,34 +0,0 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Analytics::UniqueVisitsHelper do
include Devise::Test::ControllerHelpers
describe '#track_visit' do
let(:target_id) { 'p_analytics_valuestream' }
let(:current_user) { create(:user) }
it 'does not track visit if user is not logged in' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit)
helper.track_visit(target_id)
end
it 'tracks visit if user is logged in' do
sign_in(current_user)
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit)
helper.track_visit(target_id)
end
it 'tracks visit if user is not logged in, but has the cookie already' do
helper.request.cookies[:visitor_id] = { value: SecureRandom.uuid, expires: 24.months }
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit)
helper.track_visit(target_id)
end
end
end

View file

@ -1,81 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state do
let(:unique_visits) { Gitlab::Analytics::UniqueVisits.new }
let(:target1_id) { 'g_analytics_contribution' }
let(:target2_id) { 'g_analytics_insights' }
let(:target3_id) { 'g_analytics_issues' }
let(:target4_id) { 'g_compliance_dashboard' }
let(:target5_id) { 'i_compliance_credential_inventory' }
let(:visitor1_id) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' }
let(:visitor2_id) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' }
let(:visitor3_id) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' }
around do |example|
# We need to freeze to a reference time
# because visits are grouped by the week number in the year
# Without freezing the time, the test may behave inconsistently
# depending on which day of the week test is run.
reference_time = Time.utc(2020, 6, 1)
travel_to(reference_time) { example.run }
end
describe '#track_visit' do
it 'tracks the unique weekly visits for targets' do
unique_visits.track_visit(target1_id, values: visitor1_id, time: 7.days.ago)
unique_visits.track_visit(target1_id, values: visitor1_id, time: 7.days.ago)
unique_visits.track_visit(target1_id, values: visitor2_id, time: 7.days.ago)
unique_visits.track_visit(target2_id, values: visitor2_id, time: 7.days.ago)
unique_visits.track_visit(target2_id, values: visitor1_id, time: 8.days.ago)
unique_visits.track_visit(target2_id, values: visitor1_id, time: 15.days.ago)
unique_visits.track_visit(target4_id, values: visitor3_id, time: 7.days.ago)
unique_visits.track_visit(target5_id, values: visitor3_id, time: 15.days.ago)
unique_visits.track_visit(target5_id, values: visitor2_id, time: 15.days.ago)
expect(unique_visits.unique_visits_for(targets: target1_id)).to eq(2)
expect(unique_visits.unique_visits_for(targets: target2_id)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target4_id)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target2_id, start_date: 15.days.ago)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target3_id)).to eq(0)
expect(unique_visits.unique_visits_for(targets: target5_id, start_date: 15.days.ago)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :analytics)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :analytics, start_date: 15.days.ago)).to eq(1)
expect(unique_visits.unique_visits_for(targets: :analytics, start_date: 30.days.ago)).to eq(0)
expect(unique_visits.unique_visits_for(targets: :analytics, start_date: 4.weeks.ago, end_date: Date.current)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :compliance)).to eq(1)
expect(unique_visits.unique_visits_for(targets: :compliance, start_date: 15.days.ago)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :compliance, start_date: 30.days.ago)).to eq(0)
expect(unique_visits.unique_visits_for(targets: :compliance, start_date: 4.weeks.ago, end_date: Date.current)).to eq(2)
end
it 'sets the keys in Redis to expire automatically after 12 weeks' do
unique_visits.track_visit(target1_id, values: visitor1_id)
Gitlab::Redis::SharedState.with do |redis|
redis.scan_each(match: "{#{target1_id}}-*").each do |key|
expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
end
end
end
it 'raises an error if an invalid target id is given' do
invalid_target_id = "x_invalid"
expect do
unique_visits.track_visit(invalid_target_id, values: visitor1_id)
end.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
end
end
end

View file

@ -240,18 +240,12 @@ RSpec.describe AtomicInternalId do
end
describe '.with_project_iid_supply' do
let(:iid) { 100 }
it 'wraps generate and track_greatest in a concurrency-safe lock' do
expect_next_instance_of(InternalId::InternalIdGenerator) do |g|
expect(g).to receive(:with_lock).and_call_original
expect(g.record).to receive(:last_value).and_return(iid)
expect(g).to receive(:track_greatest).with(iid + 4)
end
::Milestone.with_project_iid_supply(milestone.project) do |supply|
4.times { supply.next_value }
end
it 'supplies a stream of iid values' do
expect do
::Milestone.with_project_iid_supply(milestone.project) do |supply|
4.times { supply.next_value }
end
end.to change { InternalId.find_by(project: milestone.project, usage: :milestones)&.last_value.to_i }.by(4)
end
end
end

View file

@ -39,12 +39,144 @@ RSpec.describe InternalId do
end
end
describe '.generate_next' do
subject { described_class.generate_next(id_subject, scope, usage, init) }
shared_examples_for 'a monotonically increasing id generator' do
describe '.generate_next' do
subject { described_class.generate_next(id_subject, scope, usage, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
it 'stores record attributes' do
subject
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
end
end
context 'with existing issues' do
before do
create_list(:issue, 2, project: project)
described_class.delete_all
end
it 'calculates last_value values automatically' do
expect(subject).to eq(project.issues.size + 1)
end
end
end
it 'generates a strictly monotone, gapless sequence' do
seq = Array.new(10).map do
described_class.generate_next(issue, scope, usage, init)
end
normalized = seq.map { |i| i - seq.min }
expect(normalized).to eq((0..seq.size - 1).to_a)
end
context 'there are no instances to pass in' do
let(:id_subject) { Issue }
it 'accepts classes instead' do
expect(subject).to eq(1)
end
end
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
end
end
end
describe '.reset' do
subject { described_class.reset(issue, scope, usage, value) }
context 'in the absence of a record' do
let(:value) { 2 }
it 'does not revert back the value' do
expect { subject }.not_to change { described_class.count }
expect(subject).to be_falsey
end
end
context 'when valid iid is used to reset' do
let!(:value) { generate_next }
context 'and iid is a latest one' do
it 'does rewind and next generated value is the same' do
expect(subject).to be_truthy
expect(generate_next).to eq(value)
end
end
context 'and iid is not a latest one' do
it 'does not rewind' do
generate_next
expect(subject).to be_falsey
expect(generate_next).to be > value
end
end
def generate_next
described_class.generate_next(issue, scope, usage, init)
end
end
context 'when executed outside of transaction' do
let(:value) { 2 }
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
context 'when executed within transaction' do
let(:value) { 2 }
it 'increments counter with in_transaction: "true"' do
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
end
end
end
describe '.track_greatest' do
let(:value) { 9001 }
subject { described_class.track_greatest(id_subject, scope, usage, value, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
end
it 'stores record attributes' do
@ -53,200 +185,69 @@ RSpec.describe InternalId do
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
end
context 'with existing issues' do
before do
create_list(:issue, 2, project: project)
create(:issue, project: project)
described_class.delete_all
end
it 'calculates last_value values automatically' do
expect(subject).to eq(project.issues.size + 1)
it 'still returns the last value to that of the given value' do
expect(subject).to eq(value)
end
end
context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] }
record = double
expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present
expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process
expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
expect(record).to receive(:increment_and_save!)
context 'when value is less than the current last_value' do
it 'returns the current last_value' do
described_class.create!(**scope, usage: usage, last_value: 10_001)
expect(subject).to eq 10_001
end
end
context 'there are no instances to pass in' do
let(:id_subject) { Issue }
it 'accepts classes instead' do
expect(subject).to eq(value)
end
end
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
end
it 'generates a strictly monotone, gapless sequence' do
seq = Array.new(10).map do
described_class.generate_next(issue, scope, usage, init)
end
normalized = seq.map { |i| i - seq.min }
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
expect(InternalId.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'true').and_call_original
expect(normalized).to eq((0..seq.size - 1).to_a)
end
context 'there are no instances to pass in' do
let(:id_subject) { Issue }
it 'accepts classes instead' do
expect(subject).to eq(1)
end
end
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
InternalId.transaction { subject }
end
end
end
end
describe '.reset' do
subject { described_class.reset(issue, scope, usage, value) }
context 'when the feature flag is disabled' do
stub_feature_flags(generate_iids_without_explicit_locking: false)
context 'in the absence of a record' do
let(:value) { 2 }
it 'does not revert back the value' do
expect { subject }.not_to change { described_class.count }
expect(subject).to be_falsey
end
end
context 'when valid iid is used to reset' do
let!(:value) { generate_next }
context 'and iid is a latest one' do
it 'does rewind and next generated value is the same' do
expect(subject).to be_truthy
expect(generate_next).to eq(value)
end
end
context 'and iid is not a latest one' do
it 'does not rewind' do
generate_next
expect(subject).to be_falsey
expect(generate_next).to be > value
end
end
def generate_next
described_class.generate_next(issue, scope, usage, init)
end
end
context 'when executed outside of transaction' do
let(:value) { 2 }
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
context 'when executed within transaction' do
let(:value) { 2 }
it 'increments counter with in_transaction: "true"' do
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
end
end
it_behaves_like 'a monotonically increasing id generator'
end
describe '.track_greatest' do
let(:value) { 9001 }
context 'when the feature flag is enabled' do
stub_feature_flags(generate_iids_without_explicit_locking: true)
subject { described_class.track_greatest(id_subject, scope, usage, value, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
end
it 'stores record attributes' do
subject
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
end
context 'with existing issues' do
before do
create(:issue, project: project)
described_class.delete_all
end
it 'still returns the last value to that of the given value' do
expect(subject).to eq(value)
end
end
context 'when value is less than the current last_value' do
it 'returns the current last_value' do
described_class.create!(**scope, usage: usage, last_value: 10_001)
expect(subject).to eq 10_001
end
end
context 'there are no instances to pass in' do
let(:id_subject) { Issue }
it 'accepts classes instead' do
expect(subject).to eq(value)
end
end
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original
subject
end
end
context 'when executed within transaction' do
it 'increments counter with in_transaction: "true"' do
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'true').and_call_original
InternalId.transaction { subject }
end
end
it_behaves_like 'a monotonically increasing id generator'
end
describe '#increment_and_save!' do

View file

@ -165,9 +165,9 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
3.times { supply.next_value }
end
current_value = described_class.public_send(method_name, scope_value, &:current_value)
expect(current_value).to eq(iid + 3)
described_class.public_send(method_name, scope_value) do |supply|
expect(supply.next_value).to eq(iid + 4)
end
end
end