Work towards fixing embed images (#174)
* Add Redis and Sidekiq * Rename PreviewGenerationMonitor * Update production.rb require master key * Initialize AWS at application start * Add fallbacks for credentials * Add logging * Create railway.toml
This commit is contained in:
parent
e3a44ca0d5
commit
1c1ed0dd9d
13 changed files with 191 additions and 56 deletions
6
Gemfile
6
Gemfile
|
|
@ -47,6 +47,12 @@ gem 'email_validator'
|
||||||
# pg_search builds ActiveRecord named scopes that take advantage of PostgreSQL’s full text search
|
# pg_search builds ActiveRecord named scopes that take advantage of PostgreSQL’s full text search
|
||||||
gem 'pg_search'
|
gem 'pg_search'
|
||||||
|
|
||||||
|
# A Ruby client library for Redis
|
||||||
|
gem 'redis'
|
||||||
|
|
||||||
|
# Simple, efficient background processing for Ruby
|
||||||
|
gem 'sidekiq'
|
||||||
|
|
||||||
# scheduler for Ruby (at, in, cron and every jobs)
|
# scheduler for Ruby (at, in, cron and every jobs)
|
||||||
gem 'rufus-scheduler'
|
gem 'rufus-scheduler'
|
||||||
|
|
||||||
|
|
|
||||||
11
Gemfile.lock
11
Gemfile.lock
|
|
@ -299,6 +299,10 @@ GEM
|
||||||
rbs (2.8.4)
|
rbs (2.8.4)
|
||||||
rdoc (6.10.0)
|
rdoc (6.10.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
redis (5.3.0)
|
||||||
|
redis-client (>= 0.22.0)
|
||||||
|
redis-client (0.23.2)
|
||||||
|
connection_pool
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
reline (0.6.0)
|
reline (0.6.0)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
|
|
@ -351,6 +355,11 @@ GEM
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
shoulda-matchers (6.4.0)
|
shoulda-matchers (6.4.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
|
sidekiq (7.3.7)
|
||||||
|
connection_pool (>= 2.3.0)
|
||||||
|
logger
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
redis-client (>= 0.22.2)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
|
|
@ -444,6 +453,7 @@ DEPENDENCIES
|
||||||
puma
|
puma
|
||||||
rack-cors
|
rack-cors
|
||||||
rails
|
rails
|
||||||
|
redis
|
||||||
responders
|
responders
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rspec_junit_formatter
|
rspec_junit_formatter
|
||||||
|
|
@ -451,6 +461,7 @@ DEPENDENCIES
|
||||||
rufus-scheduler
|
rufus-scheduler
|
||||||
sdoc
|
sdoc
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
|
sidekiq
|
||||||
simplecov
|
simplecov
|
||||||
solargraph
|
solargraph
|
||||||
spring
|
spring
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ class GeneratePartyPreviewJob < ApplicationJob
|
||||||
around_perform :track_timing
|
around_perform :track_timing
|
||||||
|
|
||||||
def perform(party_id)
|
def perform(party_id)
|
||||||
# Log start of job processing
|
|
||||||
Rails.logger.info("Starting preview generation for party #{party_id}")
|
Rails.logger.info("Starting preview generation for party #{party_id}")
|
||||||
|
|
||||||
party = Party.find(party_id)
|
party = Party.find(party_id)
|
||||||
|
|
||||||
if party.preview_state == 'generated' &&
|
if party.preview_state == 'generated' &&
|
||||||
|
|
@ -26,7 +24,10 @@ class GeneratePartyPreviewJob < ApplicationJob
|
||||||
|
|
||||||
begin
|
begin
|
||||||
service = PreviewService::Coordinator.new(party)
|
service = PreviewService::Coordinator.new(party)
|
||||||
|
Rails.logger.info("Created PreviewService::Coordinator")
|
||||||
|
|
||||||
result = service.generate_preview
|
result = service.generate_preview
|
||||||
|
Rails.logger.info("Generate preview result: #{result}")
|
||||||
|
|
||||||
if result
|
if result
|
||||||
Rails.logger.info("Successfully generated preview for party #{party_id}")
|
Rails.logger.info("Successfully generated preview for party #{party_id}")
|
||||||
|
|
@ -36,9 +37,10 @@ class GeneratePartyPreviewJob < ApplicationJob
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error("Error generating preview for party #{party_id}: #{e.message}")
|
Rails.logger.error("Error generating preview for party #{party_id}: #{e.message}")
|
||||||
Rails.logger.error(e.backtrace.join("\n"))
|
Rails.logger.error("Full error details:")
|
||||||
|
Rails.logger.error(e.full_message) # This will include the stack trace
|
||||||
notify_failure(party, e)
|
notify_failure(party, e)
|
||||||
raise # Allow retry mechanism to handle the error
|
raise
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'aws-sdk-s3'
|
|
||||||
|
|
||||||
class AwsService
|
class AwsService
|
||||||
attr_reader :s3_client, :bucket
|
attr_reader :s3_client, :bucket
|
||||||
|
|
||||||
class ConfigurationError < StandardError; end
|
class ConfigurationError < StandardError; end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
validate_credentials!
|
Rails.logger.info "Environment: #{Rails.env}"
|
||||||
|
|
||||||
|
# Try different methods of getting credentials
|
||||||
|
creds = get_credentials
|
||||||
|
Rails.logger.info "Credentials source: #{creds[:source]}"
|
||||||
|
|
||||||
@s3_client = Aws::S3::Client.new(
|
@s3_client = Aws::S3::Client.new(
|
||||||
region: Rails.application.credentials.dig(:aws, :region),
|
region: creds[:region],
|
||||||
access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
|
access_key_id: creds[:access_key_id],
|
||||||
secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key)
|
secret_access_key: creds[:secret_access_key]
|
||||||
)
|
)
|
||||||
@bucket = Rails.application.credentials.dig(:aws, :bucket_name)
|
@bucket = creds[:bucket_name]
|
||||||
rescue KeyError => e
|
rescue KeyError => e
|
||||||
raise ConfigurationError, "Missing AWS credential: #{e.message}"
|
raise ConfigurationError, "Missing AWS credential: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
@ -40,30 +40,59 @@ class AwsService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def credentials
|
def get_credentials
|
||||||
@credentials ||= begin
|
# Try Rails credentials first
|
||||||
creds = Rails.application.credentials[:aws]
|
rails_creds = Rails.application.credentials.dig(:aws)
|
||||||
raise ConfigurationError, 'AWS credentials not found' unless creds
|
if rails_creds&.dig(:access_key_id)
|
||||||
|
Rails.logger.info "Using Rails credentials"
|
||||||
{
|
return rails_creds.merge(source: 'rails_credentials')
|
||||||
region: creds[:region],
|
|
||||||
access_key_id: creds[:access_key_id],
|
|
||||||
secret_access_key: creds[:secret_access_key],
|
|
||||||
bucket_name: creds[:bucket_name]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_credentials!
|
|
||||||
missing = []
|
|
||||||
creds = Rails.application.credentials[:aws]
|
|
||||||
|
|
||||||
%i[region access_key_id secret_access_key bucket_name].each do |key|
|
|
||||||
missing << key unless creds&.dig(key)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless missing.any?
|
# Try string keys
|
||||||
|
rails_creds = Rails.application.credentials.dig('aws')
|
||||||
|
if rails_creds&.dig('access_key_id')
|
||||||
|
Rails.logger.info "Using Rails credentials (string keys)"
|
||||||
|
return {
|
||||||
|
region: rails_creds['region'],
|
||||||
|
access_key_id: rails_creds['access_key_id'],
|
||||||
|
secret_access_key: rails_creds['secret_access_key'],
|
||||||
|
bucket_name: rails_creds['bucket_name'],
|
||||||
|
source: 'rails_credentials_string'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
raise ConfigurationError, "Missing AWS credentials: #{missing.join(', ')}"
|
# Try environment variables
|
||||||
|
if ENV['AWS_ACCESS_KEY_ID']
|
||||||
|
Rails.logger.info "Using environment variables"
|
||||||
|
return {
|
||||||
|
region: ENV['AWS_REGION'],
|
||||||
|
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
|
||||||
|
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
|
||||||
|
bucket_name: ENV['AWS_BUCKET_NAME'],
|
||||||
|
source: 'environment'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Try alternate environment variable names
|
||||||
|
if ENV['RAILS_AWS_ACCESS_KEY_ID']
|
||||||
|
Rails.logger.info "Using Rails-prefixed environment variables"
|
||||||
|
return {
|
||||||
|
region: ENV['RAILS_AWS_REGION'],
|
||||||
|
access_key_id: ENV['RAILS_AWS_ACCESS_KEY_ID'],
|
||||||
|
secret_access_key: ENV['RAILS_AWS_SECRET_ACCESS_KEY'],
|
||||||
|
bucket_name: ENV['RAILS_AWS_BUCKET_NAME'],
|
||||||
|
source: 'rails_environment'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
validate_credentials = ->(creds, source) {
|
||||||
|
missing = []
|
||||||
|
%i[region access_key_id secret_access_key bucket_name].each do |key|
|
||||||
|
missing << key unless creds[key].present?
|
||||||
|
end
|
||||||
|
raise ConfigurationError, "Missing AWS credentials from #{source}: #{missing.join(', ')}" if missing.any?
|
||||||
|
}
|
||||||
|
|
||||||
|
raise ConfigurationError, "No AWS credentials found in any location"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,24 @@ module PreviewService
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_blank_canvas(width: PREVIEW_WIDTH, height: PREVIEW_HEIGHT, color: DEFAULT_BACKGROUND_COLOR)
|
def create_blank_canvas(width: PREVIEW_WIDTH, height: PREVIEW_HEIGHT, color: DEFAULT_BACKGROUND_COLOR)
|
||||||
temp_file = Tempfile.new(%w[canvas .png])
|
Rails.logger.info("Checking ImageMagick installation...")
|
||||||
|
version = `convert -version`
|
||||||
|
Rails.logger.info("ImageMagick version: #{version}")
|
||||||
|
|
||||||
MiniMagick::Tool::Convert.new do |convert|
|
temp_file = Tempfile.new(%w[canvas .png])
|
||||||
convert.size "#{width}x#{height}"
|
Rails.logger.info("Created temp file: #{temp_file.path}")
|
||||||
convert << "xc:#{color}"
|
|
||||||
convert << temp_file.path
|
begin
|
||||||
|
MiniMagick::Tool::Convert.new do |convert|
|
||||||
|
convert.size "#{width}x#{height}"
|
||||||
|
convert << "xc:#{color}"
|
||||||
|
convert << temp_file.path
|
||||||
|
end
|
||||||
|
Rails.logger.info("Canvas created successfully")
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error("Failed to create canvas: #{e.message}")
|
||||||
|
Rails.logger.error(e.backtrace.join("\n"))
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
temp_file
|
temp_file
|
||||||
|
|
|
||||||
|
|
@ -41,22 +41,33 @@ module PreviewService
|
||||||
Rails.logger.info("Starting preview generation for party #{@party.id}")
|
Rails.logger.info("Starting preview generation for party #{@party.id}")
|
||||||
set_generation_in_progress
|
set_generation_in_progress
|
||||||
|
|
||||||
# Generate the preview image
|
Rails.logger.info("Creating preview image...")
|
||||||
image = create_preview_image
|
image = create_preview_image
|
||||||
save_preview(image)
|
Rails.logger.info("Preview image created successfully")
|
||||||
|
|
||||||
# Update party state
|
Rails.logger.info("Saving preview...")
|
||||||
|
save_preview(image)
|
||||||
|
Rails.logger.info("Preview saved successfully")
|
||||||
|
|
||||||
|
Rails.logger.info("Updating party state...")
|
||||||
@party.update!(
|
@party.update!(
|
||||||
preview_state: :generated,
|
preview_state: :generated,
|
||||||
preview_generated_at: Time.current
|
preview_generated_at: Time.current
|
||||||
)
|
)
|
||||||
|
Rails.logger.info("Party state updated successfully")
|
||||||
|
|
||||||
true
|
true
|
||||||
rescue => e
|
rescue => e
|
||||||
|
Rails.logger.error("Failed to generate preview: #{e.class} - #{e.message}")
|
||||||
|
Rails.logger.error("Stack trace:")
|
||||||
|
Rails.logger.error(e.backtrace.join("\n"))
|
||||||
handle_preview_generation_error(e)
|
handle_preview_generation_error(e)
|
||||||
false
|
false
|
||||||
ensure
|
ensure
|
||||||
|
Rails.logger.info("Cleaning up resources...")
|
||||||
@image_fetcher.cleanup
|
@image_fetcher.cleanup
|
||||||
clear_generation_in_progress
|
clear_generation_in_progress
|
||||||
|
Rails.logger.info("Cleanup completed")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -99,28 +110,34 @@ module PreviewService
|
||||||
#
|
#
|
||||||
# @return [MiniMagick::Image] The generated preview image
|
# @return [MiniMagick::Image] The generated preview image
|
||||||
def create_preview_image
|
def create_preview_image
|
||||||
# Create blank canvas
|
Rails.logger.info("Creating blank canvas...")
|
||||||
canvas = @canvas_service.create_blank_canvas
|
canvas = @canvas_service.create_blank_canvas
|
||||||
image = MiniMagick::Image.new(canvas.path)
|
image = MiniMagick::Image.new(canvas.path)
|
||||||
|
Rails.logger.info("Blank canvas created")
|
||||||
|
|
||||||
# Fetch job icon
|
Rails.logger.info("Processing job icon...")
|
||||||
job_icon = nil
|
job_icon = nil
|
||||||
if @party.job.present?
|
if @party.job.present?
|
||||||
|
Rails.logger.info("Fetching job icon for job ID: #{@party.job.granblue_id}")
|
||||||
job_icon = @image_fetcher.fetch_job_icon(@party.job.granblue_id)
|
job_icon = @image_fetcher.fetch_job_icon(@party.job.granblue_id)
|
||||||
|
Rails.logger.info("Job icon fetched successfully") if job_icon
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add party name with job icon
|
Rails.logger.info("Adding party name and job icon...")
|
||||||
text_result = @canvas_service.add_text(image, @party.name, job_icon: job_icon, user: @party.user)
|
text_result = @canvas_service.add_text(image, @party.name, job_icon: job_icon, user: @party.user)
|
||||||
image = text_result[:image]
|
image = text_result[:image]
|
||||||
|
Rails.logger.info("Party name and job icon added")
|
||||||
|
|
||||||
# Calculate grid layout
|
Rails.logger.info("Calculating grid layout...")
|
||||||
grid_layout = @grid_service.calculate_layout(
|
grid_layout = @grid_service.calculate_layout(
|
||||||
canvas_height: Canvas::PREVIEW_HEIGHT,
|
canvas_height: Canvas::PREVIEW_HEIGHT,
|
||||||
title_bottom_y: text_result[:text_bottom_y]
|
title_bottom_y: text_result[:text_bottom_y]
|
||||||
)
|
)
|
||||||
|
Rails.logger.info("Grid layout calculated")
|
||||||
|
|
||||||
# Organize and draw weapons
|
Rails.logger.info("Drawing weapons...")
|
||||||
image = organize_and_draw_weapons(image, grid_layout)
|
image = organize_and_draw_weapons(image, grid_layout)
|
||||||
|
Rails.logger.info("Weapons drawn successfully")
|
||||||
|
|
||||||
image
|
image
|
||||||
end
|
end
|
||||||
|
|
@ -196,15 +213,22 @@ module PreviewService
|
||||||
begin
|
begin
|
||||||
image.write(temp_file.path)
|
image.write(temp_file.path)
|
||||||
|
|
||||||
|
# Use timestamped filename similar to local storage
|
||||||
|
timestamp = Time.current.strftime('%Y%m%d%H%M%S')
|
||||||
|
key = "#{PREVIEW_FOLDER}/#{@party.shortcode}_#{timestamp}.png"
|
||||||
|
|
||||||
File.open(temp_file.path, 'rb') do |file|
|
File.open(temp_file.path, 'rb') do |file|
|
||||||
@aws_service.s3_client.put_object(
|
@aws_service.s3_client.put_object(
|
||||||
bucket: S3_BUCKET,
|
bucket: @aws_service.bucket,
|
||||||
key: preview_key,
|
key: key,
|
||||||
body: file,
|
body: file,
|
||||||
content_type: 'image/png',
|
content_type: 'image/png',
|
||||||
acl: 'private'
|
acl: 'private'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Optionally, store this key on the party record if needed for retrieval
|
||||||
|
@party.update!(preview_s3_key: key)
|
||||||
ensure
|
ensure
|
||||||
temp_file.close
|
temp_file.close
|
||||||
temp_file.unlink
|
temp_file.unlink
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# app/services/preview_generation_monitor.rb
|
require 'sidekiq/api'
|
||||||
|
|
||||||
module PreviewService
|
module PreviewService
|
||||||
class PreviewGenerationMonitor
|
class GenerationMonitor
|
||||||
class << self
|
class << self
|
||||||
def check_stalled_jobs
|
def check_stalled_jobs
|
||||||
Party.where(preview_state: :queued)
|
Party.where(preview_state: :queued)
|
||||||
|
|
@ -2,7 +2,8 @@ require "active_support/core_ext/integer/time"
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
config.require_master_key = true
|
||||||
|
|
||||||
# Code is not reloaded between requests.
|
# Code is not reloaded between requests.
|
||||||
config.cache_classes = true
|
config.cache_classes = true
|
||||||
|
|
||||||
|
|
|
||||||
10
config/initializers/aws.rb
Normal file
10
config/initializers/aws.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Rails.application.config.after_initialize do
|
||||||
|
Rails.logger.info "Initializing AWS Service..."
|
||||||
|
begin
|
||||||
|
AwsService.new
|
||||||
|
Rails.logger.info "AWS Service initialized successfully"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to initialize AWS Service: #{e.message}"
|
||||||
|
Rails.logger.error e.backtrace.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
9
config/initializers/redis.rb
Normal file
9
config/initializers/redis.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Fetch environment variables with defaults if not set
|
||||||
|
redis_url = ENV.fetch('REDIS_URL', 'redis://localhost')
|
||||||
|
redis_port = ENV.fetch('REDISPORT', '6379')
|
||||||
|
|
||||||
|
# Combine URL and port (adjust the path/DB as needed)
|
||||||
|
full_redis_url = "#{redis_url}/0"
|
||||||
|
|
||||||
|
# Initialize Redis using the constructed URL
|
||||||
|
$redis = Redis.new(url: full_redis_url)
|
||||||
|
|
@ -5,14 +5,14 @@ unless defined?(Rails::Console) || Rails.env.test? || File.split($0).last == 'ra
|
||||||
scheduler = Rufus::Scheduler.new
|
scheduler = Rufus::Scheduler.new
|
||||||
|
|
||||||
scheduler.every '5m' do
|
scheduler.every '5m' do
|
||||||
PreviewGenerationMonitor.check_stalled_jobs
|
PreviewService::GenerationMonitor.check_stalled_jobs
|
||||||
end
|
end
|
||||||
|
|
||||||
scheduler.every '1h' do
|
scheduler.every '1h' do
|
||||||
PreviewGenerationMonitor.retry_failed
|
PreviewService::GenerationMonitor.retry_failed
|
||||||
end
|
end
|
||||||
|
|
||||||
scheduler.every '1d' do
|
scheduler.every '1d' do
|
||||||
PreviewGenerationMonitor.cleanup_old_previews
|
PreviewService::GenerationMonitor.cleanup_old_previews
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
14
config/initializers/sidekiq.rb
Normal file
14
config/initializers/sidekiq.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Fetch environment variables with defaults if not set
|
||||||
|
redis_url = ENV.fetch('REDIS_URL', 'redis://localhost')
|
||||||
|
redis_port = ENV.fetch('REDISPORT', '6379')
|
||||||
|
|
||||||
|
# Combine URL and port (adjust the path/DB as needed)
|
||||||
|
full_redis_url = "#{redis_url}/0"
|
||||||
|
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.redis = { url: full_redis_url }
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq.configure_client do |config|
|
||||||
|
config.redis = { url: full_redis_url }
|
||||||
|
end
|
||||||
16
railway.toml
Normal file
16
railway.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[phases.setup]
|
||||||
|
nixPkgs = [
|
||||||
|
"imagemagick",
|
||||||
|
"ghostscript", # For PDF/PS operations
|
||||||
|
"pkgconfig", # For gem compilation
|
||||||
|
"libmagickwand" # ImageMagick C library
|
||||||
|
]
|
||||||
|
|
||||||
|
[phases.install]
|
||||||
|
dependsOn = ["setup"]
|
||||||
|
|
||||||
|
[phases.build]
|
||||||
|
dependsOn = ["install"]
|
||||||
|
|
||||||
|
[start]
|
||||||
|
cmd = "bin/rails server"
|
||||||
Loading…
Reference in a new issue