summaryrefslogtreecommitdiff
path: root/lib/gitlab/sidekiq_config/worker_matcher.rb
blob: fe5ac10c65ac0266dd626ae299bceb99e7d86ee8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# frozen_string_literal: true

module Gitlab
  module SidekiqConfig
    class WorkerMatcher
      WILDCARD_MATCH = '*'
      QUERY_OR_OPERATOR = '|'
      QUERY_AND_OPERATOR = '&'
      QUERY_CONCATENATE_OPERATOR = ','
      QUERY_TERM_REGEX = %r{^(\w+)(!?=)([\w:#{QUERY_CONCATENATE_OPERATOR}]+)}.freeze

      QUERY_PREDICATES = {
        feature_category: :to_sym,
        has_external_dependencies: lambda { |value| value == 'true' },
        name: :to_s,
        resource_boundary: :to_sym,
        tags: :to_sym,
        urgency: :to_sym
      }.freeze

      QueryError = Class.new(StandardError)
      InvalidTerm = Class.new(QueryError)
      UnknownOperator = Class.new(QueryError)
      UnknownPredicate = Class.new(QueryError)

      def initialize(query_string)
        @match_lambda = query_string_to_lambda(query_string)
      end

      def match?(worker_metadata)
        @match_lambda.call(worker_metadata)
      end

      private

      def query_string_to_lambda(query_string)
        return lambda { |_worker| true } if query_string.strip == WILDCARD_MATCH

        or_clauses = query_string.split(QUERY_OR_OPERATOR).map do |and_clauses_string|
          and_clauses_predicates = and_clauses_string.split(QUERY_AND_OPERATOR).map do |term|
            predicate_for_term(term)
          end

          lambda { |worker| and_clauses_predicates.all? { |predicate| predicate.call(worker) } }
        end

        lambda { |worker| or_clauses.any? { |predicate| predicate.call(worker) } }
      end

      def predicate_for_term(term)
        match = term.match(QUERY_TERM_REGEX)

        raise InvalidTerm.new("Invalid term: #{term}") unless match

        _, lhs, op, rhs = *match

        predicate_for_op(op, predicate_factory(lhs, rhs.split(QUERY_CONCATENATE_OPERATOR)))
      end

      def predicate_for_op(op, predicate)
        case op
        when '='
          predicate
        when '!='
          lambda { |worker| !predicate.call(worker) }
        else
          # This is unreachable because InvalidTerm will be raised instead, but
          # keeping it allows to guard against that changing in future.
          raise UnknownOperator.new("Unknown operator: #{op}")
        end
      end

      def predicate_factory(lhs, values)
        values_block = QUERY_PREDICATES[lhs.to_sym]

        raise UnknownPredicate.new("Unknown predicate: #{lhs}") unless values_block

        lambda do |queue|
          comparator = Array(queue[lhs.to_sym]).to_set

          values.map(&values_block).to_set.intersect?(comparator)
        end
      end
    end
  end
end