Ausgabe
In einer Rails 6.x-App habe ich eine Controller-Methode, die Abfragen, die länger als 2 Minuten dauern, in den Hintergrund stellt (um ein Browser-Timeout zu vermeiden), den Benutzer berät, die Ergebnisse speichert und einen Link sendet, der sie abrufen kann, um ein Live zu generieren Seite (mit Highcharts-Charts). Das funktioniert gut.
Jetzt versuche ich, die gleiche Logik mit einer Methode zu implementieren, die die Erstellung eines Berichts über eine Tempfile im Hintergrund durchführt und den Inhalt an eine E-Mail anhängt, wenn die Abfrage zu lange läuft. Dieser Code funktioniert gut, wenn das 2-Minuten-Timeout NICHT erreicht wird, aber die Tempfile ist in der kommentierten Zeile leer, wenn das Timeout erreicht ist.
Ich habe versucht, den zweiten Teil in einen anderen Thread zu verpacken und die Interna jedes Threads mit einem Mutex zu verpacken, aber das geht mir alles über den Kopf. Ich habe nicht viel Multithreading gemacht, und jedes Mal, wenn ich es tue, habe ich das Gefühl, dass ich herumstolpere, bis ich es verstehe. Diesmal kann ich es nicht einmal scheinen, hineinzustolpern.
Ich weiß nicht, ob das Problem mit meinen Threads oder einer Racebedingung mit dem Tempfile-Objekt zusammenhängt. Ich hatte schon früher Probleme mit Tempfiles, weil sie scheinbar schneller verschwinden, als ich sie schließen kann. Wird dieser hier aufgeräumt, bevor er versendet werden kann? Das Dateihandle ist am kommentierten Punkt tatsächlich noch im Dateisystem vorhanden, obwohl es leer ist, sodass mir nicht klar ist, was passiert.
def report
queue = Queue.new
file = Tempfile.new('report')
thr = Thread.new do
query = %Q(blah blah blah)
@calibrations = ActiveRecord::Base.connection.exec_query query
query = %Q(blah blah blah)
@tunings = ActiveRecord::Base.connection.exec_query query
if queue.empty?
unless @tunings.empty?
CSV.open(file.path, 'wb') do |csv|
csv << ["headers...", @parameters].flatten
@calibrations.each do |c|
line = [c["h1"], c["h2"], c["h3"], c["h4"], c["h5"], c["h6"], c["h7"], c["h8"]]
t = @tunings.select { |t| t["code"] == c["code"] }.first
@parameters.each do |parameter|
line << t[parameter.downcase]
end
csv << line
end
end
send_data file.read, :type => 'text/csv; charset=iso-8859-1; header=present', :disposition => "attachment; filename=\"report.csv\""
end
else
# When "timed out", `file` is empty here
NotificationMailer.report_ready(current_user, file.read).deliver_later
end
end
give_up_at = Time.now + 120.seconds
while Time.now < give_up_at do
if !thr.alive?
break
end
sleep 1
end
if thr.alive?
queue << "Timeout"
render html: "Your report is taking longer than 2 minutes to generate. To avoid a browser timeout, it will finish in the background, and the report will be sent to you in email."
end
end
Lösung
Die Datei ist leer, weil Sie der Abfrage 120 Sekunden zum Abschließen geben. Wenn dies nach 120 Sekunden nicht geschehen ist, fügen Sie der Warteschlange “Timeout” hinzu. Die Abfrage läuft noch innerhalb des Threads und hat noch nicht den Punkt erreicht, an dem Sie prüfen, ob die Warteschlange leer ist oder nicht. Notification.report
Wenn die Abfrage abgeschlossen ist, überspringen Sie den Teil, in dem Sie die CSV-Datei schreiben, und gehen zur Zeile , da die Warteschlange jetzt nicht leer ist . Zu diesem Zeitpunkt ist die Datei noch leer, weil Sie nie etwas hineingeschrieben haben.
Am Ende denke ich, dass Sie die Gesamtlogik dessen, was Sie zu erreichen versuchen, überdenken müssen, und es muss mehr Kommunikation zwischen den Threads und der obersten Ebene geben.
Jeder Thread muss der obersten Ebene mitteilen, ob er das Ergebnis bereits gesendet hat, und die oberste Ebene muss den Thread wissen lassen, dass es an der Zeit ist, das Ergebnis direkt zu senden, und stattdessen das Ergebnis per E-Mail senden sollte.
Hier ist ein Code, von dem ich denke / hoffe, dass er einen Einblick in die Herangehensweise an dieses Problem gibt.
timeout_limit = 10
query_times = [5, 15, 1, 15]
timeout = []
sent_response = []
send_via_email = []
puts "time out is set to #{timeout_limit} seconds"
query_times.each_with_index do |query_time, query_id|
puts "starting query #{query_id} that will take #{query_time} seconds"
timeout[query_id] = false
sent_response[query_id] = false
send_via_email[query_id] = false
Thread.new do
## do query
sleep query_time
unless timeout[query_id]
puts "query #{query_id} has completed, displaying results now"
sent_response[query_id] = true
else
puts "query #{query_id} has completed, emailing result now"
send_via_email[query_id] = true
end
end
give_up_at = Time.now + timeout_limit
while Time.now < give_up_at
break if sent_response[query_id]
sleep 1
end
unless sent_response[query_id]
puts "query #{query_id} timed out, we will email the result of your query when it is completed"
timeout[query_id] = true
end
end
# simulate server environment
loop { }
=>
time out is set to 10 seconds
starting query 0 that will take 5 seconds
query 0 has completed, displaying results now
starting query 1 that will take 15 seconds
query 1 timed out, we will email the result of your query when it is completed
starting query 2 that will take 1 seconds
query 2 has completed, displaying results now
starting query 3 that will take 15 seconds
query 1 has completed, emailing result now
query 3 timed out, we will email the result of your query when it is completed
query 3 has completed, emailing result now
Beantwortet von – nPn
Antwort geprüft von – Jay B. (FixError Admin)