Commit b04953fd authored by Shreyas Agarwal's avatar Shreyas Agarwal Committed by Tyler Amos

Introduce workers to sync Seat Link information

The SyncSeatLinkWorker gathers the max historical user count from the
start of the license to the previous day, then executes the
SyncSeatLinkRequestWorker. The SyncSeatLinkWorker is a Sidekiq
CronWorker that can be retried up to 12 times (~17 hours). It can be
retried past the original job creation day to report the correct day.

The SyncSeatLinkRequestWorker is responsible for making request to the
Subscription Portal with seat info. Because all the information
necessary to make the request is pulled from the job information, this
job can be retried beyond the original job creation day.

Using UTC time to determine when to sync seat information and what
time range to use for gathering the max user count.
parent 2e1dffc1
...@@ -545,6 +545,9 @@ Gitlab.ee do ...@@ -545,6 +545,9 @@ Gitlab.ee do
Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker' Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker'
Settings.cron_jobs['sync_seat_link_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} 0 * * *"
Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker'
end end
# #
......
...@@ -232,6 +232,8 @@ ...@@ -232,6 +232,8 @@
- 2 - 2
- - service_desk_email_receiver - - service_desk_email_receiver
- 1 - 1
- - sync_seat_link_request
- 1
- - system_hook_push - - system_hook_push
- 1 - 1
- - todos_destroyer - - todos_destroyer
......
...@@ -164,6 +164,13 @@ ...@@ -164,6 +164,13 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: cronjob:sync_seat_link
:feature_category: :analysis
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: cronjob:update_all_mirrors - :name: cronjob:update_all_mirrors
:feature_category: :source_code_management :feature_category: :source_code_management
:has_external_dependencies: :has_external_dependencies:
...@@ -563,3 +570,10 @@ ...@@ -563,3 +570,10 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: sync_seat_link_request
:feature_category: :analysis
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
# frozen_string_literal: true
class SyncSeatLinkRequestWorker
include ApplicationWorker
feature_category :analysis
idempotent!
worker_has_external_dependencies!
URI_PATH = '/api/v1/seat_links'
RequestError = Class.new(StandardError)
def perform(date, license_key, max_historical_user_count)
response = Gitlab::HTTP.post(
URI_PATH,
base_uri: EE::SUBSCRIPTIONS_URL,
headers: request_headers,
body: request_body(date, license_key, max_historical_user_count)
)
raise RequestError, request_error_message(response) unless response.success?
end
private
def request_body(date, license_key, max_historical_user_count)
{
date: date,
license_key: license_key,
max_historical_user_count: max_historical_user_count
}.to_json
end
def request_headers
{ 'Content-Type' => 'application/json' }
end
def request_error_message(response)
"Seat Link request failed! Code:#{response.code} Body:#{response.body}"
end
end
# frozen_string_literal: true
class SyncSeatLinkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :analysis
# Retry for up to approximately 17 hours
sidekiq_options retry: 12, dead: false
def perform
return unless should_sync_seats?
SyncSeatLinkRequestWorker.perform_async(
report_date.to_s,
License.current.data,
max_historical_user_count
)
end
private
# Only sync paid licenses from start date until 14 days after expiration
def should_sync_seats?
License.current &&
!License.current.trial? &&
report_date.between?(License.current.starts_at, License.current.expires_at + 14.days)
end
def max_historical_user_count
HistoricalData.max_historical_user_count(
from: License.current.starts_at,
to: report_date
)
end
def report_date
@report_date ||= Time.now.utc.yesterday.to_date
end
end
---
title: Add worker to sync paid seats info daily in the background.
merge_request: 26467
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe SyncSeatLinkRequestWorker, type: :worker do
describe '#perform' do
subject do
described_class.new.perform('2020-01-01', '123', 5)
end
let(:seat_link_url) { [EE::SUBSCRIPTIONS_URL, '/api/v1/seat_links'].join }
it 'makes an HTTP POST request with passed params' do
stub_request(:post, seat_link_url).to_return(status: 200)
subject
expect(WebMock).to have_requested(:post, seat_link_url).with(
headers: { 'Content-Type' => 'application/json' },
body: {
date: '2020-01-01',
license_key: '123',
max_historical_user_count: 5
}.to_json
)
end
context 'when the request is not successful' do
before do
stub_request(:post, seat_link_url)
.to_return(status: 400, body: '{"success":false,"error":"Bad Request"}')
end
it 'raises an error with the expected message' do
expect { subject }.to raise_error(
described_class::RequestError,
'Seat Link request failed! Code:400 Body:{"success":false,"error":"Bad Request"}'
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SyncSeatLinkWorker, type: :worker do
describe '#perform' do
def create_current_license(options = {})
License.current.destroy!
gl_license = create(:gitlab_license, options)
create(:license, data: gl_license.export)
end
context 'when current, paid license is active' do
let(:utc_time) { Time.utc(2020, 3, 12, 12, 00) }
before do
# Setting the date as 12th March 2020 12:00 UTC for tests and creating new license
create_current_license(starts_at: '2020-02-12'.to_date)
HistoricalData.create!(date: '2020-02-11'.to_date, active_user_count: 100)
HistoricalData.create!(date: '2020-02-12'.to_date, active_user_count: 10)
HistoricalData.create!(date: '2020-02-13'.to_date, active_user_count: 15)
HistoricalData.create!(date: '2020-03-12'.to_date, active_user_count: 20)
HistoricalData.create!(date: '2020-03-15'.to_date, active_user_count: 25)
allow(SyncSeatLinkRequestWorker).to receive(:perform_async).and_return(true)
end
it 'executes the SyncSeatLinkRequestWorker with expected params' do
Timecop.travel(utc_time) do
subject.perform
expect(SyncSeatLinkRequestWorker).to have_received(:perform_async)
.with(
'2020-03-11',
License.current.data,
15
)
end
end
context 'when the timezone makes date one day in advance' do
before do
Time.zone = 'Auckland'
end
it 'executes the SyncSeatLinkRequestWorker with expected params' do
Timecop.travel(utc_time) do
expect(Date.current.to_s).to eql('2020-03-13')
subject.perform
expect(SyncSeatLinkRequestWorker).to have_received(:perform_async)
.with(
'2020-03-11',
License.current.data,
15
)
end
end
context 'when the timezone makes date one day before than UTC' do
before do
Time.zone = 'Central America'
end
it 'executes the SyncSeatLinkRequestWorker with expected params' do
Timecop.travel(utc_time.beginning_of_day) do
expect(Date.current.to_s).to eql('2020-03-11')
subject.perform
expect(SyncSeatLinkRequestWorker).to have_received(:perform_async)
.with(
'2020-03-11',
License.current.data,
15
)
end
end
end
end
end
shared_examples 'no seat link sync' do
it 'does not execute the SyncSeatLinkRequestWorker' do
expect(SyncSeatLinkRequestWorker).not_to receive(:perform_async)
subject.perform
end
end
context 'when license is missing' do
before do
License.current.destroy!
end
include_examples 'no seat link sync'
end
context 'when using a trial license' do
before do
create(:license, trial: true)
end
include_examples 'no seat link sync'
end
context 'when using an expired license' do
before do
create_current_license(expires_at: expiration_date)
end
context 'the license expired over 15 days ago' do
let(:expiration_date) { Time.now.utc.to_date - 16.days }
include_examples 'no seat link sync'
end
context 'the license expired less than or equal to 15 days ago' do
let(:expiration_date) { Time.now.utc.to_date - 15.days }
it 'executes the SyncSeatLinkRequestWorker' do
expect(SyncSeatLinkRequestWorker).to receive(:perform_async).and_return(true)
subject.perform
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment