dawarich/app/services/tracks/parallel_generator.rb

183 lines
4.7 KiB
Ruby

# frozen_string_literal: true
# Main orchestrator service for parallel track generation
# Coordinates time chunking, job scheduling, and session management
class Tracks::ParallelGenerator
include Tracks::Segmentation
include Tracks::TrackBuilder
attr_reader :user, :start_at, :end_at, :mode, :chunk_size
def initialize(user, start_at: nil, end_at: nil, mode: :bulk, chunk_size: 1.day)
@user = user
@start_at = start_at
@end_at = end_at
@mode = mode.to_sym
@chunk_size = chunk_size
end
def call
# Clean existing tracks if needed
clean_existing_tracks if should_clean_tracks?
# Generate time chunks
time_chunks = generate_time_chunks
return 0 if time_chunks.empty?
# Create session for tracking progress
session = create_generation_session(time_chunks.size)
# Enqueue chunk processing jobs
enqueue_chunk_jobs(session.session_id, time_chunks)
# Enqueue boundary resolver job (with delay to let chunks complete)
enqueue_boundary_resolver(session.session_id, time_chunks.size)
Rails.logger.info "Started parallel track generation for user #{user.id} with #{time_chunks.size} chunks (session: #{session.session_id})"
session
end
private
def should_clean_tracks?
case mode
when :bulk, :daily then true
else false
end
end
def generate_time_chunks
chunker = Tracks::TimeChunker.new(
user,
start_at: start_at,
end_at: end_at,
chunk_size: chunk_size
)
chunker.call
end
def create_generation_session(total_chunks)
metadata = {
mode: mode.to_s,
chunk_size: humanize_duration(chunk_size),
start_at: start_at&.iso8601,
end_at: end_at&.iso8601,
user_settings: {
time_threshold_minutes: time_threshold_minutes,
distance_threshold_meters: distance_threshold_meters
}
}
session_manager = Tracks::SessionManager.create_for_user(user.id, metadata)
session_manager.mark_started(total_chunks)
session_manager
end
def enqueue_chunk_jobs(session_id, time_chunks)
time_chunks.each do |chunk|
Tracks::TimeChunkProcessorJob.perform_later(
user.id,
session_id,
chunk
)
end
end
def enqueue_boundary_resolver(session_id, chunk_count)
# Delay based on estimated processing time (30 seconds per chunk + buffer)
estimated_delay = [chunk_count * 30.seconds, 5.minutes].max
Tracks::BoundaryResolverJob.set(wait: estimated_delay).perform_later(
user.id,
session_id
)
end
def clean_existing_tracks
case mode
when :bulk then clean_bulk_tracks
when :daily then clean_daily_tracks
else
raise ArgumentError, "Unknown mode: #{mode}"
end
end
def clean_bulk_tracks
if time_range_defined?
user.tracks.where(start_at: time_range).destroy_all
else
user.tracks.destroy_all
end
end
def clean_daily_tracks
# For daily mode, we don't want to clean all tracks for the day
# Instead, we clean tracks that overlap with the time range we're processing
# This allows for incremental processing without losing existing tracks
return unless time_range_defined?
# Only clean tracks that overlap with our processing time range
user.tracks.where(start_at: time_range).destroy_all
end
def time_range_defined?
start_at.present? || end_at.present?
end
def time_range
return nil unless time_range_defined?
start_time = start_at&.to_i
end_time = end_at&.to_i
if start_time && end_time
Time.zone.at(start_time)..Time.zone.at(end_time)
elsif start_time
Time.zone.at(start_time)..
elsif end_time
..Time.zone.at(end_time)
end
end
def daily_time_range
day = start_at&.to_date || Date.current
day.beginning_of_day.to_i..day.end_of_day.to_i
end
def distance_threshold_meters
@distance_threshold_meters ||= user.safe_settings.meters_between_routes.to_i
end
def time_threshold_minutes
@time_threshold_minutes ||= user.safe_settings.minutes_between_routes.to_i
end
def humanize_duration(duration)
case duration
when 1.day then '1 day'
when 1.hour then '1 hour'
when 6.hours then '6 hours'
when 12.hours then '12 hours'
when 2.days then '2 days'
when 1.week then '1 week'
else
# Convert seconds to readable format
seconds = duration.to_i
if seconds >= 86400 # days
days = seconds / 86400
"#{days} day#{'s' if days != 1}"
elsif seconds >= 3600 # hours
hours = seconds / 3600
"#{hours} hour#{'s' if hours != 1}"
elsif seconds >= 60 # minutes
minutes = seconds / 60
"#{minutes} minute#{'s' if minutes != 1}"
else
"#{seconds} second#{'s' if seconds != 1}"
end
end
end
end