Aggiunge pulizia ciclica segmenti locali e doc tabelloni/overlay.
Evita il riempimento disco da segmenti MediaMTX post-sessione con cron orario; documenta come board, overlay e regolamenti sportivi si relazionano nel sistema. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
backend/app/jobs/recordings/cleanup_local_job.rb
Normal file
14
backend/app/jobs/recordings/cleanup_local_job.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Recordings
|
||||
class CleanupLocalJob
|
||||
include Sidekiq::Job
|
||||
sidekiq_options retry: 1, queue: "default"
|
||||
|
||||
def perform
|
||||
result = Recordings::CleanupLocal.new.call
|
||||
Rails.logger.info(
|
||||
"[Recordings::CleanupLocalJob] removed=#{result.removed} " \
|
||||
"freed=#{result.freed_bytes} skipped=#{result.skipped}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
103
backend/app/services/recordings/cleanup_local.rb
Normal file
103
backend/app/services/recordings/cleanup_local.rb
Normal file
@@ -0,0 +1,103 @@
|
||||
module Recordings
|
||||
class CleanupLocal
|
||||
ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze
|
||||
REMOVABLE_RECORDING_STATUSES = %w[ready failed expired].freeze
|
||||
|
||||
Result = Struct.new(:removed, :freed_bytes, :skipped, keyword_init: true)
|
||||
|
||||
def initialize(online_paths: nil, logger: Rails.logger)
|
||||
@online_paths = online_paths
|
||||
@logger = logger
|
||||
@removed = 0
|
||||
@freed_bytes = 0
|
||||
@skipped = 0
|
||||
end
|
||||
|
||||
def call
|
||||
online = @online_paths || Mediamtx::Client.new.online_path_names
|
||||
|
||||
each_live_match_dir do |dir_path, session_id, path_name|
|
||||
if online.include?(path_name)
|
||||
@skipped += 1
|
||||
next
|
||||
end
|
||||
|
||||
session = StreamSession.find_by(id: session_id)
|
||||
unless removable?(session)
|
||||
@skipped += 1
|
||||
next
|
||||
end
|
||||
|
||||
remove_dir(dir_path)
|
||||
cleanup_mediamtx_path(session) if session
|
||||
end
|
||||
|
||||
Result.new(removed: @removed, freed_bytes: @freed_bytes, skipped: @skipped)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def each_live_match_dir
|
||||
base = File.join(MatchLiveTv.recordings_local_path, "live")
|
||||
return unless Dir.exist?(base)
|
||||
|
||||
Dir.children(base).sort.each do |entry|
|
||||
next unless entry.start_with?("match_")
|
||||
|
||||
session_id = entry.delete_prefix("match_")
|
||||
path_name = "live/#{entry}"
|
||||
dir_path = File.join(base, entry)
|
||||
next unless File.directory?(dir_path)
|
||||
|
||||
yield dir_path, session_id, path_name
|
||||
end
|
||||
end
|
||||
|
||||
def removable?(session)
|
||||
return true if session.nil?
|
||||
|
||||
return false if ACTIVE_SESSION_STATUSES.include?(session.status)
|
||||
|
||||
recording = Recording.find_by(stream_session: session)
|
||||
return true if recording&.status.in?(REMOVABLE_RECORDING_STATUSES)
|
||||
|
||||
return false unless session.status.in?(%w[ended error])
|
||||
|
||||
ended_at = session.ended_at || session.updated_at
|
||||
grace = recording&.status == "processing" ? processing_grace : ended_grace
|
||||
ended_at < grace.ago
|
||||
end
|
||||
|
||||
def processing_grace
|
||||
ENV.fetch("RECORDINGS_LOCAL_PROCESSING_GRACE_HOURS", "6").to_i.hours
|
||||
end
|
||||
|
||||
def ended_grace
|
||||
ENV.fetch("RECORDINGS_LOCAL_ENDED_GRACE_HOURS", "2").to_i.hours
|
||||
end
|
||||
|
||||
def remove_dir(path)
|
||||
size = dir_size(path)
|
||||
FileUtils.rm_rf(path)
|
||||
@removed += 1
|
||||
@freed_bytes += size
|
||||
@logger.info("[Recordings::CleanupLocal] removed #{path} (#{size} bytes)")
|
||||
rescue StandardError => e
|
||||
@logger.warn("[Recordings::CleanupLocal] failed #{path}: #{e.message}")
|
||||
end
|
||||
|
||||
def dir_size(path)
|
||||
`du -sb #{Shellwords.escape(path)} 2>/dev/null`.split.first.to_i
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
|
||||
def cleanup_mediamtx_path(session)
|
||||
return unless session.status.in?(%w[ended error])
|
||||
|
||||
Mediamtx::Client.new.delete_path(session)
|
||||
rescue Mediamtx::Client::Error => e
|
||||
@logger.warn("[Recordings::CleanupLocal] delete_path #{session.id}: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,4 +10,11 @@ namespace :recordings do
|
||||
Recordings::ExpiryWarningJob.new.perform
|
||||
puts "Avvisi scadenza replay inviati"
|
||||
end
|
||||
|
||||
desc "Rimuove segmenti MediaMTX locali di sessioni terminate (libera disco)"
|
||||
task cleanup_local: :environment do
|
||||
result = Recordings::CleanupLocal.new.call
|
||||
puts "Pulizia segmenti locali: rimossi=#{result.removed} " \
|
||||
"liberati=#{result.freed_bytes} byte saltati=#{result.skipped}"
|
||||
end
|
||||
end
|
||||
|
||||
82
backend/spec/services/recordings/cleanup_local_spec.rb
Normal file
82
backend/spec/services/recordings/cleanup_local_spec.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Recordings::CleanupLocal do
|
||||
let!(:club) { Club.create!(name: "Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||
let!(:team) { club.teams.create!(name: "Team", sport: "volleyball") }
|
||||
let!(:user) { User.create!(email: "coach@test.com", name: "Coach", password: "password123", role: "coach") }
|
||||
let!(:match) { team.matches.create!(opponent_name: "Rival", scheduled_at: 1.day.from_now) }
|
||||
|
||||
let(:recordings_root) { Rails.root.join("tmp/recordings_spec").to_s }
|
||||
let(:live_root) { File.join(recordings_root, "live") }
|
||||
|
||||
around do |example|
|
||||
FileUtils.mkdir_p(live_root)
|
||||
original = ENV["RECORDINGS_PATH"]
|
||||
ENV["RECORDINGS_PATH"] = recordings_root
|
||||
example.run
|
||||
ensure
|
||||
ENV["RECORDINGS_PATH"] = original
|
||||
FileUtils.rm_rf(recordings_root)
|
||||
end
|
||||
|
||||
def write_segment(session_id)
|
||||
dir = File.join(live_root, "match_#{session_id}")
|
||||
FileUtils.mkdir_p(dir)
|
||||
File.write(File.join(dir, "segment.mp4"), "x" * 1024)
|
||||
dir
|
||||
end
|
||||
|
||||
it "rimuove segmenti di sessioni terminate con replay pronto" do
|
||||
session = StreamSession.create!(
|
||||
match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 1.hour.ago
|
||||
)
|
||||
write_segment(session.id)
|
||||
Recording.create!(
|
||||
stream_session: session, team: team, status: "ready", privacy_status: "public",
|
||||
storage_key: "teams/#{team.id}/sessions/#{session.id}/replay.mp4"
|
||||
)
|
||||
|
||||
result = described_class.new(online_paths: []).call
|
||||
|
||||
expect(result.removed).to eq(1)
|
||||
expect(Dir.exist?(File.join(live_root, "match_#{session.id}"))).to be(false)
|
||||
end
|
||||
|
||||
it "non rimuove segmenti di sessioni ancora in onda" do
|
||||
session = StreamSession.create!(
|
||||
match: match, user: user, platform: "matchlivetv", status: "live"
|
||||
)
|
||||
write_segment(session.id)
|
||||
|
||||
result = described_class.new(online_paths: [session.mediamtx_path_name]).call
|
||||
|
||||
expect(result.removed).to eq(0)
|
||||
expect(result.skipped).to eq(1)
|
||||
expect(Dir.exist?(File.join(live_root, "match_#{session.id}"))).to be(true)
|
||||
end
|
||||
|
||||
it "non rimuove segmenti di sessioni terminate di recente in elaborazione" do
|
||||
session = StreamSession.create!(
|
||||
match: match, user: user, platform: "matchlivetv", status: "ended", ended_at: 30.minutes.ago
|
||||
)
|
||||
write_segment(session.id)
|
||||
Recording.create!(
|
||||
stream_session: session, team: team, status: "processing", privacy_status: "public"
|
||||
)
|
||||
|
||||
result = described_class.new(online_paths: []).call
|
||||
|
||||
expect(result.removed).to eq(0)
|
||||
expect(Dir.exist?(File.join(live_root, "match_#{session.id}"))).to be(true)
|
||||
end
|
||||
|
||||
it "rimuove cartelle orfane senza sessione nel database" do
|
||||
orphan_id = SecureRandom.uuid
|
||||
write_segment(orphan_id)
|
||||
|
||||
result = described_class.new(online_paths: []).call
|
||||
|
||||
expect(result.removed).to eq(1)
|
||||
expect(Dir.exist?(File.join(live_root, "match_#{orphan_id}"))).to be(false)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user