[FIXED] Wie kann ich verhindern, dass der Inhalt der Tempfile in einem separaten (Ruby-)Thread leer ist?

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.reportWenn 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)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like