Class: Ci::Runner

Constant Summary collapse

ONLINE_CONTACT_TIMEOUT =

This `ONLINE_CONTACT_TIMEOUT` needs to be larger than

`RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY`
2.hours
RUNNER_QUEUE_EXPIRY_TIME =

The `RUNNER_QUEUE_EXPIRY_TIME` indicates the longest interval that

Runner request needs to be refreshed by Rails instead of being handled
by Workhorse
1.hour
UPDATE_CONTACT_COLUMN_EVERY =

The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated

(40.minutes..55.minutes).freeze
STALE_TIMEOUT =

The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner will be considered stale

3.months
AVAILABLE_TYPES_LEGACY =
%w[specific shared].freeze
AVAILABLE_TYPES =
runner_types.keys.freeze
AVAILABLE_STATUSES =
%w[active paused online offline not_connected never_contacted stale].freeze
AVAILABLE_SCOPES =
(AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE =
%i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
MINUTES_COST_FACTOR_FIELDS =
%i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
TAG_LIST_MAX_LENGTH =
50

Constants included from TaggableQueries

TaggableQueries::MAX_TAGS_IDS, TaggableQueries::TooManyTagsError

Constants included from RedisCacheable

RedisCacheable::CACHED_ATTRIBUTES_EXPIRY_TIME

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Presentable

#present

Methods included from TaggableQueries

#tags_ids

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from FeatureGate

#flipper_id

Methods included from ChronicDurationAttribute

#chronic_duration_attributes, #output_chronic_duration_attribute

Methods included from RedisCacheable

#cache_attributes, #cached_attribute

Methods inherited from ApplicationRecord

model_name, table_name_prefix

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Class Method Details

.online_contact_time_deadlineObject


220
221
222
# File 'app/models/ci/runner.rb', line 220

def self.online_contact_time_deadline
  ONLINE_CONTACT_TIMEOUT.ago
end

.order_by(order) ⇒ Object


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'app/models/ci/runner.rb', line 236

def self.order_by(order)
  case order
  when 'contacted_asc'
    order_contacted_at_asc
  when 'contacted_desc'
    order_contacted_at_desc
  when 'created_at_asc'
    order_created_at_asc
  when 'token_expires_at_asc'
    order_token_expires_at_asc
  when 'token_expires_at_desc'
    order_token_expires_at_desc
  else
    order_created_at_desc
  end
end

.recent_queue_deadlineObject


228
229
230
231
232
233
234
# File 'app/models/ci/runner.rb', line 228

def self.recent_queue_deadline
  # we add queue expiry + online
  # - contacted_at can be updated at any time within this interval
  #   we have always accurate `contacted_at` but it is stored in Redis
  #   and not persisted in database
  (ONLINE_CONTACT_TIMEOUT + RUNNER_QUEUE_EXPIRY_TIME).ago
end

.runner_matchersObject


253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'app/models/ci/runner.rb', line 253

def self.runner_matchers
  unique_params = [
    :runner_type,
    :public_projects_minutes_cost_factor,
    :private_projects_minutes_cost_factor,
    :run_untagged,
    :access_level,
    Arel.sql("(#{arel_tag_names_array.to_sql})")
  ]

  group(*unique_params).pluck('array_agg(ci_runners.id)', *unique_params).map do |values|
    Gitlab::Ci::Matching::RunnerMatcher.new({
      runner_ids: values[0],
      runner_type: values[1],
      public_projects_minutes_cost_factor: values[2],
      private_projects_minutes_cost_factor: values[3],
      run_untagged: values[4],
      access_level: values[5],
      tag_list: values[6]
    })
  end
end

.search(query) ⇒ Object

Searches for runners matching the given query.

This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.

query - The search query as a String.

Returns an ActiveRecord::Relation.


216
217
218
# File 'app/models/ci/runner.rb', line 216

def self.search(query)
  where(token: query).or(fuzzy_search(query, [:description]))
end

.stale_deadlineObject


224
225
226
# File 'app/models/ci/runner.rb', line 224

def self.stale_deadline
  STALE_TIMEOUT.ago
end

.token_expiration_enforced?Boolean

Returns:

  • (Boolean)

467
468
469
# File 'app/models/ci/runner.rb', line 467

def self.token_expiration_enforced?
  Feature.enabled?(:enforce_runner_token_expires_at)
end

Instance Method Details

#assign_to(project, current_user = nil) ⇒ Object


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'app/models/ci/runner.rb', line 290

def assign_to(project, current_user = nil)
  if instance_type?
    raise ArgumentError, 'Transitioning an instance runner to a project runner is not supported'
  elsif group_type?
    raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
  end

  begin
    transaction do
      self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self)
      self.save!
    end
  rescue ActiveRecord::RecordInvalid => e
    self.errors.add(:assign_to, e.message)
    false
  end
end

#assigned_to_group?Boolean

Returns:

  • (Boolean)

356
357
358
# File 'app/models/ci/runner.rb', line 356

def assigned_to_group?
  runner_namespaces.any?
end

#assigned_to_project?Boolean

Returns:

  • (Boolean)

360
361
362
# File 'app/models/ci/runner.rb', line 360

def assigned_to_project?
  runner_projects.any?
end

#belongs_to_more_than_one_project?Boolean

Returns:

  • (Boolean)

352
353
354
# File 'app/models/ci/runner.rb', line 352

def belongs_to_more_than_one_project?
  runner_projects.limit(2).count(:all) > 1
end

#belongs_to_one_project?Boolean

Returns:

  • (Boolean)

348
349
350
# File 'app/models/ci/runner.rb', line 348

def belongs_to_one_project?
  runner_projects.count == 1
end

#compute_token_expirationObject


456
457
458
459
460
461
462
463
464
465
# File 'app/models/ci/runner.rb', line 456

def compute_token_expiration
  case runner_type
  when 'instance_type'
    compute_token_expiration_instance
  when 'group_type'
    compute_token_expiration_group
  when 'project_type'
    compute_token_expiration_project
  end
end

#deprecated_rest_statusObject

DEPRECATED TODO Remove in %16.0 in favor of `status` for REST calls, see gitlab.com/gitlab-org/gitlab/-/issues/344648


338
339
340
341
342
343
344
345
346
# File 'app/models/ci/runner.rb', line 338

def deprecated_rest_status
  if contacted_at.nil?
    :not_connected
  elsif active?
    online? ? :online : :offline
  else
    :paused
  end
end

#display_nameObject


308
309
310
311
312
# File 'app/models/ci/runner.rb', line 308

def display_name
  return short_sha if description.blank?

  description
end

#ensure_runner_queue_valueObject


410
411
412
413
414
# File 'app/models/ci/runner.rb', line 410

def ensure_runner_queue_value
  new_value = SecureRandom.hex
  ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value,
    expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
end

#has_tags?Boolean

Returns:

  • (Boolean)

384
385
386
# File 'app/models/ci/runner.rb', line 384

def has_tags?
  tag_list.any?
end

#heartbeat(values) ⇒ Object


420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'app/models/ci/runner.rb', line 420

def heartbeat(values)
  ##
  # We can safely ignore writes performed by a runner heartbeat. We do
  # not want to upgrade database connection proxy to use the primary
  # database after heartbeat write happens.
  #
  ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
    values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {}
    values[:contacted_at] = Time.current
    values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)

    cache_attributes(values)

    # We save data without validation, it will always change due to `contacted_at`
    self.update_columns(values) if persist_cached_data?
  end
end

#match_build_if_online?(build) ⇒ Boolean

Returns:

  • (Boolean)

364
365
366
# File 'app/models/ci/runner.rb', line 364

def match_build_if_online?(build)
  active? && online? && matches_build?(build)
end

#matches_build?(build) ⇒ Boolean

Returns:

  • (Boolean)

442
443
444
# File 'app/models/ci/runner.rb', line 442

def matches_build?(build)
  runner_matcher.matches?(build.build_matcher)
end

#namespace_idsObject


450
451
452
453
454
# File 'app/models/ci/runner.rb', line 450

def namespace_ids
  strong_memoize(:namespace_ids) do
    runner_namespaces.pluck(:namespace_id).compact
  end
end

#online?Boolean

Returns:

  • (Boolean)

314
315
316
# File 'app/models/ci/runner.rb', line 314

def online?
  contacted_at && contacted_at > self.class.online_contact_time_deadline
end

#only_for?(project) ⇒ Boolean

Returns:

  • (Boolean)

368
369
370
# File 'app/models/ci/runner.rb', line 368

def only_for?(project)
  !runner_projects.where.not(project_id: project.id).exists?
end

#pick_build!(build) ⇒ Object


438
439
440
# File 'app/models/ci/runner.rb', line 438

def pick_build!(build)
  tick_runner_queue if matches_build?(build)
end

#predefined_variablesObject


388
389
390
391
392
393
# File 'app/models/ci/runner.rb', line 388

def predefined_variables
  Gitlab::Ci::Variables::Collection.new
    .append(key: 'CI_RUNNER_ID', value: id.to_s)
    .append(key: 'CI_RUNNER_DESCRIPTION', value: description)
    .append(key: 'CI_RUNNER_TAGS', value: tag_list.to_s)
end

#runner_matcherObject


276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'app/models/ci/runner.rb', line 276

def runner_matcher
  strong_memoize(:runner_matcher) do
    Gitlab::Ci::Matching::RunnerMatcher.new({
      runner_ids: [id],
      runner_type: runner_type,
      public_projects_minutes_cost_factor: public_projects_minutes_cost_factor,
      private_projects_minutes_cost_factor: private_projects_minutes_cost_factor,
      run_untagged: run_untagged,
      access_level: access_level,
      tag_list: tag_list
    })
  end
end

#runner_queue_value_latest?(value) ⇒ Boolean

Returns:

  • (Boolean)

416
417
418
# File 'app/models/ci/runner.rb', line 416

def runner_queue_value_latest?(value)
  ensure_runner_queue_value == value if value.present?
end

#short_shaObject


372
373
374
# File 'app/models/ci/runner.rb', line 372

def short_sha
  token[0...8] if token
end

#stale?Boolean

Returns:

  • (Boolean)

318
319
320
321
322
# File 'app/models/ci/runner.rb', line 318

def stale?
  return false unless created_at

  [created_at, contacted_at].compact.max < self.class.stale_deadline
end

#status(legacy_mode = nil) ⇒ Object


324
325
326
327
328
329
330
331
332
333
334
# File 'app/models/ci/runner.rb', line 324

def status(legacy_mode = nil)
  # TODO Deprecate legacy_mode in %16.0 and make it a no-op
  #   (see https://gitlab.com/gitlab-org/gitlab/-/issues/360545)
  # TODO Remove legacy_mode in %17.0
  return deprecated_rest_status if legacy_mode == '14.5'

  return :stale if stale?
  return :never_contacted unless contacted_at

  online? ? :online : :offline
end

#tag_listObject


376
377
378
379
380
381
382
# File 'app/models/ci/runner.rb', line 376

def tag_list
  if tags.loaded?
    tags.map(&:name)
  else
    super
  end
end

#tick_runner_queueObject


395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'app/models/ci/runner.rb', line 395

def tick_runner_queue
  ##
  # We only stick a runner to primary database to be able to detect the
  # replication lag in `EE::Ci::RegisterJobService#execute`. The
  # intention here is not to execute `Ci::RegisterJobService#execute` on
  # the primary database.
  #
  ::Ci::Runner.sticking.stick(:runner, id)

  SecureRandom.hex.tap do |new_update|
    ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
      expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true)
  end
end

#uncached_contacted_atObject


446
447
448
# File 'app/models/ci/runner.rb', line 446

def uncached_contacted_at
  read_attribute(:contacted_at)
end