Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Realtime trophy check #187

Merged
merged 10 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ gem 'rails', '~> 7.1.3.2'
gem 'propshaft'

# Use postgresql as the database for Active Record
gem 'pg', '~> 1.1'
gem 'pg', '~> 1.5'
# Use Puma as the app server
gem 'puma', '~> 5.0'
# Use Active Model has_secure_password
Expand Down Expand Up @@ -56,3 +56,5 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem 'rqrcode', '~> 2.2'

gem 'rexml', '~> 3.2'

gem 'solid_queue', '~> 0.3.0'
17 changes: 15 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,16 @@ GEM
erb-formatter (0.4.3)
syntax_tree (~> 6.0)
erubi (1.12.0)
et-orbi (1.2.11)
tzinfo
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (3.1.0)
net-http
ffi (1.15.5)
fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
hashie (5.0.0)
Expand Down Expand Up @@ -178,7 +183,7 @@ GEM
parallel (1.22.1)
parser (3.2.1.0)
ast (~> 2.4.1)
pg (1.4.6)
pg (1.5.6)
prettier_print (1.2.1)
propshaft (0.8.0)
actionpack (>= 7.0.0)
Expand All @@ -190,6 +195,7 @@ GEM
public_suffix (5.0.4)
puma (5.6.5)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.2)
rack (2.2.8.1)
rack-contrib (2.3.0)
Expand Down Expand Up @@ -273,6 +279,12 @@ GEM
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
solid_queue (0.3.0)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (~> 1.2.2)
fugit (~> 1.9.0)
railties (>= 7.1)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
Expand Down Expand Up @@ -324,7 +336,7 @@ DEPENDENCIES
listen (~> 3.3)
omniauth-github (~> 2.0)
omniauth-rails_csrf_protection (~> 1.0)
pg (~> 1.1)
pg (~> 1.5)
propshaft
puma (~> 5.0)
rack-contrib (~> 2.3.0)
Expand All @@ -334,6 +346,7 @@ DEPENDENCIES
rqrcode (~> 2.2)
rubocop (~> 1.18)
selenium-webdriver
solid_queue (~> 0.3.0)
stimulus-rails (~> 1.3)
tailwindcss-rails (~> 2.0)
turbo-rails (~> 2.0)
Expand Down
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
release: fc-cache -fv
web: bundle exec puma -C config/puma.rb
worker: bundle exec rake solid_queue:start
1 change: 1 addition & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch
css: bin/rails tailwindcss:watch
worker: bundle exec rake solid_queue:start
6 changes: 6 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class ApplicationController < ActionController::Base

around_action :with_time_zone

after_action :check_trophy

rescue_from Exception, with: :server_error
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::RoutingError, with: :not_found
Expand Down Expand Up @@ -102,4 +104,8 @@ def breakout_turbo
session[:breakout_turbo] = nil
@breakout_turbo = true
end

def check_trophy
TrophyJob.perform_later(@user.profile) if @user&.profile
end
end
9 changes: 9 additions & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
11 changes: 11 additions & 0 deletions app/jobs/trophy_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class TrophyJob < ApplicationJob
queue_as :default

def perform(profile)
Trigger.where('description LIKE ?', 'trophy:%').each do |trigger|
trigger.perform(profile, 'trophy')
end
end
end
4 changes: 3 additions & 1 deletion app/models/trigger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class ExpiresError < TriggerError; end

validate :check_action_is_json_string

def perform(target, given_key)
def perform(target, given_key = '')
return unless Conditions.new(conditions, target).satisfy?

check_key(given_key)
check_amount
check_expires
Expand Down
116 changes: 116 additions & 0 deletions app/models/trigger/condition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

class Trigger
class Condition
UndefinedActionError = Class.new(TriggerError)
UndefinedOperatorError = Class.new(TriggerError)
InvalidModelError = Class.new(TriggerError)

attr_reader :condition, :target, :props, :attribute

def initialize(condition, target)
@condition = condition
@target = target
end

def satisfy?
@props = condition[:props].transform_values { transform_target(_1) }
check
end

private

def transform_target(value)
case Array(value).map(&:to_sym)
in [:target]
target_model
in [:target, *attrs]
attrs.inject(target_model) { |acc, attr| acc&.public_send(attr) }
else
value
end
end

def check
cond = condition[:action].is_a?(Hash) ? condition[:action].deep_symbolize_keys : condition[:action].to_sym
@attribute = cond.delete(:attribute) if cond.is_a?(Hash)

case cond
in :exists
count_check(0, 'gt')
in :not_exists
count_check(0, 'eq')
in { compare: op, value: value}
compare_check(op, value)
in { count: num, operator: op }
count_check(num, op)
in { includes: includes }
includes_check(includes)
else
raise UndefinedActionError, "Action '#{condition[:action]}' is not defined.'"
end
end

def subject_model
@subject_model ||= if condition[:eager_load]
inflate_model(condition[:model]).eager_load(condition[:eager_load])
else
inflate_model(condition[:model])
end
end

def inflate_model(model_name)
model = model_name.constantize
return model if model.ancestors.include?(ApplicationRecord)

raise InvalidModelError, "#{model} is not subclass of ApplicationRecord."
end

def target_model
@target_model ||= if @target.instance_of?(inflate_model(condition[:target]))
@target
else
raise(InvalidModelError, "#{@target.class} is not match target #{condition[:target]}")
end
end

def resolve_model_attribute
objects = subject_model.where(props)
Array(@attribute).inject(objects) { |acc, attr| acc.map { _1&.public_send(attr) } }
end

def compare_check(operator, value)
subject = resolve_model_attribute.first
case operator
when 'eq'
subject == value
when 'not_eq'
subject != value
end
end

def count_check(num, operator)
count = resolve_model_attribute.count
case operator
when 'eq'
count == num
when 'gt'
count > num
when 'gteq'
count >= num
when 'lt'
count < num
when 'lteq'
count <= num
else
raise UndefinedOperatorError, "Operator '#{operator}' is not defined."
end
end

def includes_check(includes)
resolve_model_attribute.each do |obj|
return false unless includes.include?(obj)
end
end
end
end
20 changes: 20 additions & 0 deletions app/models/trigger/conditions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class Trigger
class Conditions
attr_reader :conditions, :target

def initialize(conditions, target)
@conditions = conditions
@target = target
end

def satisfy?
return true if conditions.empty?

conditions.each do |condition|
return false unless Condition.new(condition.deep_symbolize_keys, target).satisfy?
end
end
end
end
1 change: 1 addition & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
config.active_job.queue_adapter = :solid_queue

# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
Expand Down
1 change: 1 addition & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
config.active_job.queue_adapter = :solid_queue

# Code is not reloaded between requests.
config.cache_classes = true
Expand Down
18 changes: 18 additions & 0 deletions config/solid_queue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 5
processes: 1
polling_interval: 0.1

development:
<<: *default

test:
<<: *default

production:
<<: *default
Loading
Loading