Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
63de36be
Commit
63de36be
authored
Sep 16, 2021
by
Fabio Pitino
Committed by
Bob Van Landuyt
Sep 16, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Prepare CI minutes consumption update to be idempotent
parent
40a74511
Changes
4
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
237 additions
and
129 deletions
+237
-129
ee/app/services/ci/minutes/update_project_and_namespace_usage_service.rb
.../ci/minutes/update_project_and_namespace_usage_service.rb
+46
-13
ee/app/workers/ci/minutes/update_project_and_namespace_usage_worker.rb
...s/ci/minutes/update_project_and_namespace_usage_worker.rb
+4
-2
ee/spec/services/ci/minutes/update_project_and_namespace_usage_service_spec.rb
...inutes/update_project_and_namespace_usage_service_spec.rb
+126
-95
ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb
...minutes/update_project_and_namespace_usage_worker_spec.rb
+61
-19
No files found.
ee/app/services/ci/minutes/update_project_and_namespace_usage_service.rb
View file @
63de36be
...
@@ -5,9 +5,12 @@ module Ci
...
@@ -5,9 +5,12 @@ module Ci
class
UpdateProjectAndNamespaceUsageService
class
UpdateProjectAndNamespaceUsageService
include
Gitlab
::
Utils
::
StrongMemoize
include
Gitlab
::
Utils
::
StrongMemoize
def
initialize
(
project_id
,
namespace_id
)
IDEMPOTENCY_CACHE_TTL
=
12
.
hours
def
initialize
(
project_id
,
namespace_id
,
build_id
=
nil
)
@project_id
=
project_id
@project_id
=
project_id
@namespace_id
=
namespace_id
@namespace_id
=
namespace_id
@build_id
=
build_id
# TODO(issue 335885): Use project_id only and don't query for projects which may be deleted
# TODO(issue 335885): Use project_id only and don't query for projects which may be deleted
@project
=
Project
.
find_by_id
(
project_id
)
@project
=
Project
.
find_by_id
(
project_id
)
end
end
...
@@ -16,22 +19,24 @@ module Ci
...
@@ -16,22 +19,24 @@ module Ci
def
execute
(
consumption
)
def
execute
(
consumption
)
legacy_track_usage_of_monthly_minutes
(
consumption
)
legacy_track_usage_of_monthly_minutes
(
consumption
)
preload_minutes_usage_data!
# TODO: fix this condition after the next deployment when `build_id`
# is made a mandatory argument.
ApplicationRecord
.
transaction
do
# https://gitlab.com/gitlab-org/gitlab/-/issues/331785
if
@build_id
ensure_idempotency
{
track_usage_of_monthly_minutes
(
consumption
)
}
else
track_usage_of_monthly_minutes
(
consumption
)
track_usage_of_monthly_minutes
(
consumption
)
send_minutes_email_notification
end
end
end
private
send_minutes_email_notification
end
def
preload_minutes_usage_data!
def
idempotency_cache_key
project_usage
"ci_minutes_usage:
#{
@project_id
}
:
#{
@build_id
}
:updated"
namespace_usage
end
end
private
def
send_minutes_email_notification
def
send_minutes_email_notification
# `perform reset` on `project` because `Namespace#namespace_statistics` will otherwise return stale data.
# `perform reset` on `project` because `Namespace#namespace_statistics` will otherwise return stale data.
# TODO(issue 335885): Remove @project
# TODO(issue 335885): Remove @project
...
@@ -46,8 +51,14 @@ module Ci
...
@@ -46,8 +51,14 @@ module Ci
end
end
def
track_usage_of_monthly_minutes
(
consumption
)
def
track_usage_of_monthly_minutes
(
consumption
)
::
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
increase_usage
(
namespace_usage
,
consumption
)
if
namespace_usage
# preload minutes usage data outside of transaction
::
Ci
::
Minutes
::
ProjectMonthlyUsage
.
increase_usage
(
project_usage
,
consumption
)
if
project_usage
project_usage
namespace_usage
::
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
transaction
do
::
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
increase_usage
(
namespace_usage
,
consumption
)
if
namespace_usage
::
Ci
::
Minutes
::
ProjectMonthlyUsage
.
increase_usage
(
project_usage
,
consumption
)
if
project_usage
end
end
end
def
update_legacy_project_minutes
(
consumption_in_seconds
)
def
update_legacy_project_minutes
(
consumption_in_seconds
)
...
@@ -88,6 +99,28 @@ module Ci
...
@@ -88,6 +99,28 @@ module Ci
rescue
ActiveRecord
::
NotNullViolation
,
ActiveRecord
::
RecordInvalid
rescue
ActiveRecord
::
NotNullViolation
,
ActiveRecord
::
RecordInvalid
end
end
end
end
# Ensure we only add the CI minutes consumption once for the given build
# even if the worker is retried.
def
ensure_idempotency
return
if
already_completed?
yield
mark_as_completed!
end
def
mark_as_completed!
Gitlab
::
Redis
::
SharedState
.
with
do
|
redis
|
redis
.
set
(
idempotency_cache_key
,
1
,
ex:
IDEMPOTENCY_CACHE_TTL
)
end
end
def
already_completed?
Gitlab
::
Redis
::
SharedState
.
with
do
|
redis
|
redis
.
exists
(
idempotency_cache_key
)
end
end
end
end
end
end
end
end
ee/app/workers/ci/minutes/update_project_and_namespace_usage_worker.rb
View file @
63de36be
...
@@ -9,8 +9,10 @@ module Ci
...
@@ -9,8 +9,10 @@ module Ci
urgency
:low
urgency
:low
data_consistency
:always
# primarily performs writes
data_consistency
:always
# primarily performs writes
def
perform
(
consumption
,
project_id
,
namespace_id
)
def
perform
(
consumption
,
project_id
,
namespace_id
,
build_id
=
nil
)
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
.
new
(
project_id
,
namespace_id
).
execute
(
consumption
)
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
.
new
(
project_id
,
namespace_id
,
build_id
)
.
execute
(
consumption
)
end
end
end
end
end
end
...
...
ee/spec/services/ci/minutes/update_project_and_namespace_usage_service_spec.rb
View file @
63de36be
This diff is collapsed.
Click to expand it.
ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb
View file @
63de36be
...
@@ -5,39 +5,81 @@ require 'spec_helper'
...
@@ -5,39 +5,81 @@ require 'spec_helper'
RSpec
.
describe
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageWorker
do
RSpec
.
describe
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageWorker
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:namespace
)
{
project
.
namespace
}
let_it_be
(
:namespace
)
{
project
.
namespace
}
let_it_be
(
:build
)
{
create
(
:ci_build
,
project:
project
)
}
let
(
:consumption
)
{
100
}
let
(
:consumption
)
{
100
}
let
(
:consumption_seconds
)
{
consumption
*
60
}
let
(
:consumption_seconds
)
{
consumption
*
60
}
let
(
:worker
)
{
described_class
.
new
}
let
(
:worker
)
{
described_class
.
new
}
describe
'#perform'
do
describe
'#perform'
do
it
'executes UpdateProjectAndNamespaceUsageService'
do
shared_examples
'executes the update'
do
service_instance
=
double
it
'executes UpdateProjectAndNamespaceUsageService'
do
expect
(
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
).
to
receive
(
:new
).
with
(
project
.
id
,
namespace
.
id
).
and_return
(
service_instance
)
service_instance
=
double
expect
(
service_instance
).
to
receive
(
:execute
).
with
(
consumption
)
expect
(
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
).
to
receive
(
:new
).
at_least
(
:once
).
and_return
(
service_instance
)
expect
(
service_instance
).
to
receive
(
:execute
).
at_least
(
:once
).
with
(
consumption
)
worker
.
perform
(
consumption
,
project
.
id
,
namespace
.
id
)
subject
end
it
'updates monthly usage'
do
subject
expect
(
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
find_by
(
namespace:
namespace
).
amount_used
).
to
eq
(
consumption
)
expect
(
Ci
::
Minutes
::
ProjectMonthlyUsage
.
find_by
(
project:
project
).
amount_used
).
to
eq
(
consumption
)
end
end
end
it
'updates statistics and usage'
do
shared_examples
'skips the update'
do
worker
.
perform
(
consumption
,
project
.
id
,
namespace
.
id
)
it
'does not execute UpdateProjectAndNamespaceUsageService'
do
expect
(
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
).
not_to
receive
(
:new
)
expect
(
project
.
statistics
.
reload
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
subject
expect
(
namespace
.
namespace_statistics
.
reload
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
end
expect
(
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
find_by
(
namespace:
namespace
).
amount_used
).
to
eq
(
consumption
)
expect
(
Ci
::
Minutes
::
ProjectMonthlyUsage
.
find_by
(
project:
project
).
amount_used
).
to
eq
(
consumption
)
end
end
it
'accumulates only legacy statistics on failure (behaves transactionally)'
do
context
'when build_id is not passed as parameter (param backward compatibility)'
do
allow
(
Ci
::
Minutes
::
ProjectMonthlyUsage
).
to
receive
(
:new
).
and_raise
(
StandardError
)
subject
{
worker
.
perform
(
consumption
,
project
.
id
,
namespace
.
id
)
}
it_behaves_like
'executes the update'
it
'updates legacy statistics'
do
subject
expect
(
project
.
statistics
.
reload
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
expect
(
namespace
.
reload
.
namespace_statistics
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
end
context
'does not behave idempotently'
do
subject
{
perform_multiple
([
consumption
,
project
.
id
,
namespace
.
id
],
worker:
worker
)
}
it
'executes the operation multiple times'
do
expect
(
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
).
to
receive
(
:new
).
twice
.
and_call_original
subject
expect
(
project
.
statistics
.
reload
.
shared_runners_seconds
).
to
eq
(
2
*
consumption_seconds
)
expect
(
namespace
.
reload
.
namespace_statistics
.
shared_runners_seconds
).
to
eq
(
2
*
consumption_seconds
)
expect
(
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
find_by
(
namespace:
namespace
).
amount_used
).
to
eq
(
2
*
consumption
)
expect
(
Ci
::
Minutes
::
ProjectMonthlyUsage
.
find_by
(
project:
project
).
amount_used
).
to
eq
(
2
*
consumption
)
end
end
end
context
'when build_id is passed as parameter'
,
:clean_gitlab_redis_shared_state
do
subject
{
perform_multiple
([
consumption
,
project
.
id
,
namespace
.
id
,
build
.
id
])
}
context
'behaves idempotently for monthly usage update'
do
it_behaves_like
'executes the update'
end
it
'does not behave idempotently for legacy statistics update'
do
expect
(
::
Ci
::
Minutes
::
UpdateProjectAndNamespaceUsageService
).
to
receive
(
:new
).
twice
.
and_call_original
expect
{
worker
.
perform
(
consumption
,
project
.
id
,
namespace
.
id
)
}.
to
raise_error
(
StandardError
)
subject
expect
(
project
.
reload
.
statistics
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
expect
(
project
.
statistics
.
reload
.
shared_runners_seconds
).
to
eq
(
2
*
consumption_seconds
)
expect
(
namespace
.
reload
.
namespace_statistics
.
shared_runners_seconds
).
to
eq
(
consumption_seconds
)
expect
(
namespace
.
reload
.
namespace_statistics
.
shared_runners_seconds
).
to
eq
(
2
*
consumption_seconds
)
expect
(
Ci
::
Minutes
::
NamespaceMonthlyUsage
.
find_by
(
namespace:
namespace
)).
to
eq
(
nil
)
end
expect
(
Ci
::
Minutes
::
ProjectMonthlyUsage
.
find_by
(
project:
project
)).
to
eq
(
nil
)
expect
(
::
Ci
::
Minutes
::
EmailNotificationService
).
not_to
receive
(
:new
)
end
end
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment