Class: InvQuotaWitness

Inherits:
Object
  • Object
show all
Defined in:
app/my_lib/inv_quota_witness.rb

Overview

Class representing a QuotaWitness responsible for managing inventory quotas in Redis. This class handles the creation, tracking, and consumption of inventory quotas for ticket groups.

Instance Method Summary collapse

Constructor Details

#initializeInvQuotaWitness

Note:

Uses global $inv_redis for write operations and $inv_redis_ro for read operations.

Initializes a new InvQuotaWitness with Redis connection pools.



7
8
9
10
11
12
# File 'app/my_lib/inv_quota_witness.rb', line 7

def initialize
  # Redis connection pool for write operations on inventory quotas
  @redis = $inv_redis
  # Redis connection pool for read-only operations on inventory quotas
  @redis_replica = $inv_redis_ro
end

Instance Method Details

#available?(quantity, ticket_group_id) ⇒ Boolean

Checks if the specified quantity is available for a specific ID within the given ticket group.

Parameters:

  • quantity (Integer)

    The quantity to check availability for.

  • ticket_group_id (String)

    The ticket group ID.

Returns:

  • (Boolean)

    Returns true if the specified quantity is available for ticket_group_id, otherwise false.



110
111
112
113
114
# File 'app/my_lib/inv_quota_witness.rb', line 110

def available?(quantity, ticket_group_id)
  redis.with do |conn|
    conn.scard(quota_key(ticket_group_id)).to_i >= quantity
  end
end

#delete_keys(ticket_group_id, ids = nil) ⇒ Object

Delete specific keys by ID or all keys by ticket_group_id in Redis.

Parameters:

  • ticket_group_id (String)

    The ticket group ID.

  • ids (Array<String>, nil) (defaults to: nil)

    The unique IDs to delete (optional).



54
55
56
57
58
59
60
61
62
# File 'app/my_lib/inv_quota_witness.rb', line 54

def delete_keys(ticket_group_id, ids = nil)
  redis.with do |conn|
    if ids.present?
      conn.srem(quota_key(ticket_group_id), ids)
    else
      conn.del(quota_key(ticket_group_id))
    end
  end
end

#generate_by_quota(ticket_group_id, quota, ticket_group = nil) ⇒ Array<String>

Generate unique IDs with a stock of 1 for the given quota and ticket group.

Parameters:

  • ticket_group_id (Integer)

    The ticket group ID.

  • quota (Integer)

    The quota to generate.

  • ticket_group (TicketGroup, nil) (defaults to: nil)

    The ticket group object for TTL calculation (optional for backward compatibility).

Returns:

  • (Array<String>)

    An array of unique IDs with stock 1.



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
# File 'app/my_lib/inv_quota_witness.rb', line 20

def generate_by_quota(ticket_group_id, quota, ticket_group = nil)
  return [] if quota.zero? || quota.negative?

  # Skip generation if ticket_group is provided and both dates have passed
  if ticket_group.present?
    max_date = [ticket_group.selling_end_date, ticket_group.valid_end_date].compact.max
    return [] if max_date.present? && max_date < Date.current
  end

  tickets = []
  now = Time.zone.now
  version = "#{now.to_date}-#{now.hour}-#{now.min}"
  redis.with do |conn|
    tickets = (1..quota).map { |i| "#{ticket_group_id}-#{version}-#{i}" }
    conn.sadd(quota_key(ticket_group_id), tickets)

    # Set TTL based on max of selling_end_date and valid_end_date
    if ticket_group.present?
      max_date = [ticket_group.selling_end_date, ticket_group.valid_end_date].compact.max
      if max_date.present?
        expire_at = (max_date.to_date + 1.day).end_of_day
        ttl_seconds = (expire_at - Time.current).to_i
        conn.expire(quota_key(ticket_group_id), ttl_seconds) if ttl_seconds > 0
      end
    end
  end

  tickets
end

#get_data_by_restaurant_id(restaurant_id) ⇒ Array<Hash>

Retrieves available quotas from all ticket groups for a restaurant.

Parameters:

  • restaurant_id (Integer)

    The restaurant ID.

Returns:

  • (Array<Hash>)

    Array of hashes containing ticket group IDs and their quotas.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'app/my_lib/inv_quota_witness.rb', line 144

def get_data_by_restaurant_id(restaurant_id)
  redis_replica.with do |conn|
    ticket_group_ids = conn.smembers(restaurant_ticket_groups_key(restaurant_id))

    arr = fetch_ticket_groups_data(conn, ticket_group_ids)

    zero_inv = arr.select { |h| h[:inv].value.zero? }.pluck(:id)
    if arr.blank? || zero_inv.present?
      update_restaurant_ticket_group_list([Restaurant.fetch(restaurant_id)])

      zero_inv.each do |ticket_group_id|
        ticket_group = TicketGroup.fetch(ticket_group_id)
        # Skip if ticket group is expired
        max_date = [ticket_group.selling_end_date, ticket_group.valid_end_date].compact.max
        next if max_date.present? && max_date < Date.current

        generate_by_quota(ticket_group_id, ticket_group.quota, ticket_group)
      end
    end

    arr = fetch_ticket_groups_data(conn, ticket_group_ids)

    arr.map do |h|
      quota = h[:inv].value

      {
        id: h[:id],
        quota: quota.to_s,
      }
    end
  end
end

#pick_tickets(ticket_group_id, quantity, redis_connection = nil) ⇒ Array<String>

Picks random tickets from the quota pool for a ticket group.

Parameters:

  • ticket_group_id (Integer)

    The ticket group ID.

  • quantity (Integer)

    The number of tickets to pick.

  • redis_connection (Redis, nil) (defaults to: nil)

    Optional Redis connection to use.

Returns:

  • (Array<String>)

    The selected tickets.



122
123
124
125
126
127
128
129
130
# File 'app/my_lib/inv_quota_witness.rb', line 122

def pick_tickets(ticket_group_id, quantity, redis_connection = nil)
  if redis_connection.nil?
    redis.with do |conn|
      conn.srandmember(quota_key(ticket_group_id), quantity)
    end
  else
    redis_connection.srandmember(quota_key(ticket_group_id), quantity)
  end
end

#quota_key(ticket_group_id) ⇒ String

Creates the Redis key for storing quota information for a ticket group.

Parameters:

  • ticket_group_id (Integer)

    The ticket group ID.

Returns:

  • (String)

    The Redis key for the quota.



68
69
70
# File 'app/my_lib/inv_quota_witness.rb', line 68

def quota_key(ticket_group_id)
  "#{namespace(ticket_group_id)}:quota"
end

#quotas(ticket_group_id) ⇒ Array<String>

Retrieves all quota entries for a given ticket group.

Parameters:

  • ticket_group_id (Integer)

    The ticket group ID.

Returns:

  • (Array<String>)

    An array of ticket identifiers.



76
77
78
79
80
81
82
# File 'app/my_lib/inv_quota_witness.rb', line 76

def quotas(ticket_group_id)
  members = []
  redis_replica.with do |conn|
    members = conn.smembers(quota_key(ticket_group_id))
  end
  members
end

#restaurant_ticket_groups_key(restaurant_id) ⇒ String

Generates the Redis key for tracking all ticket groups for a restaurant.

Parameters:

  • restaurant_id (Integer)

    The restaurant ID.

Returns:

  • (String)

    The Redis key for the restaurant's ticket groups.



136
137
138
# File 'app/my_lib/inv_quota_witness.rb', line 136

def restaurant_ticket_groups_key(restaurant_id)
  "RestaurantTicketGroups:#{restaurant_id}"
end

#restore_quota(ticket_group_id, tickets_to_restore) ⇒ Boolean

Restores previously consumed tickets back to the quota pool.

Parameters:

  • ticket_group_id (Integer)

    The ticket group ID.

  • tickets_to_restore (Array<String>)

    The tickets to restore to the quota.

Returns:

  • (Boolean)

    Whether the restore operation was successful.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'app/my_lib/inv_quota_witness.rb', line 89

def restore_quota(ticket_group_id, tickets_to_restore)
  success = false
  redis.with do |conn|
    watch_response = conn.watch(quota_key(ticket_group_id)) do
      # Assuming tickets_to_restore is an array of ticket identifiers
      # that were originally removed from the quota set for this ticket_group.
      conn.multi do
        success = true
        conn.sadd(quota_key(ticket_group_id), tickets_to_restore)
      end
    end
    success = false if watch_response.blank?
  end
  success
end

#transaction(keys) {|Redis| ... } ⇒ Object

Executes a Redis transaction on the specified keys.

Parameters:

  • keys (Array<String>)

    The Redis keys to watch in the transaction.

Yields:

  • (Redis)

    The Redis connection to use within the transaction.

Returns:

  • (Object)

    The result of the transaction.



218
219
220
221
222
223
224
# File 'app/my_lib/inv_quota_witness.rb', line 218

def transaction(keys, &block)
  redis.with do |conn|
    conn.watch(keys) do
      block.call(conn)
    end
  end
end

#update_restaurant_ticket_group_list(restaurants) ⇒ void

This method returns an undefined value.

Updates the list of ticket groups associated with restaurants in Redis.

Parameters:

  • restaurants (Array<Restaurant>)

    The restaurants to update ticket groups for.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'app/my_lib/inv_quota_witness.rb', line 181

def update_restaurant_ticket_group_list(restaurants)
  redis.with do |conn|
    restaurants.each do |restaurant|
      ticket_groups = restaurant.ticket_groups
      expected_ticket_group_ids = ticket_groups.pluck(:id)
      next if expected_ticket_group_ids.blank?

      old_ticket_group_ids = conn.smembers(restaurant_ticket_groups_key(restaurant.id))
      # delete old ticket groups from redis if it's not in the expected_ticket_group_ids
      conn.pipelined do |pipeline|
        old_ticket_group_ids.each do |old_ticket_group_id|
          unless expected_ticket_group_ids.include?(old_ticket_group_id)
            pipeline.srem(restaurant_ticket_groups_key(restaurant.id),
                          old_ticket_group_id)
          end
        end

        pipeline.sadd(restaurant_ticket_groups_key(restaurant.id), expected_ticket_group_ids)
      end

      # Set TTL based on the max expiration date of all ticket groups
      max_dates = ticket_groups.map { |tg| [tg.selling_end_date, tg.valid_end_date].compact.max }.compact
      if max_dates.present?
        overall_max_date = max_dates.max
        expire_at = (overall_max_date.to_date + 1.day).end_of_day
        ttl_seconds = (expire_at - Time.current).to_i
        conn.expire(restaurant_ticket_groups_key(restaurant.id), ttl_seconds) if ttl_seconds > 0
      end
    end
  end
end