mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
183 lines
4.7 KiB
Ruby
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
|