Asone Tuhid
Asone Tuhid

Reputation: 549

ruby: finish loop iteration before raising Interrupt

I'm looping through a lot of items and I want to periodically interrupt the loop to save and continue at a later time like this:

begin
  big_list.each do |i|
    # sensitive stuff
    sensitive_method(i)
    # other sensitive stuff
  end
rescue Interrupt
  # finish the current iteration
  # then do something else (save)
  # don't raise (print done)
end

By sensitive I mean that, if Interrupt is raised in the middle of an iteration, data will be corrupted so I need to guarantee that the iteration finishes before exiting.

Also, if another exception is raised, it should still finish the loop but raise it afterwards

EDIT:

Using the answer by mudasobwa in a test scenario:

while true
  result = begin
             puts "start"
             sleep 1
             puts "halfway"
             sleep 1
             puts "done\n\n"
              nil
            rescue Exception => e
              e
            end
  case result
    when Interrupt
      puts "STOPPED"
      break
    when Exception then raise result
  end
end

I get:

start
halfway
done

start
^C: /...
STOPPED

which is my exact problem, I need it to finish the loop (sleep, print halfway, sleep, print done) and only then break out (wrapping the puts, sleep... in a method does not help)

Upvotes: 0

Views: 592

Answers (3)

Michael Greenly
Michael Greenly

Reputation: 21

The Interrupt exception is raised in the main thread. If you use a worker thread to process the list it will never be interrupted. You will need a way to tell the worker thread to terminate though. Rescuing Interrupt in the main thread and setting a flag that's checked by the child can accomplish this.

BigList = (1..100)

def sensitive_method(item)
  puts "start #{item}"
  sleep 1
  puts "halfway #{item}"
  sleep 1
  puts "done #{item}"
  puts
end

@done = false

thread = Thread.new do
  begin
    BigList.each do |item|
      break if @done
      sensitive_method item
    end
  end
end

begin
  thread.join
rescue Interrupt
  @done = true
  thread.join
end

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110755

The keyword ensure, used in rescue clauses, is available for situation such as this one, where code must be executed after an exception occurs.

[-1, 0, 1].each do |i|
  begin
    puts "i=#{i} before exception"
    # <additional code>  
    n = 1/i
  rescue ZeroDivisionError => e
    puts "Exception: #{e}"
    exit
  ensure
    puts "Just executed 1/#{i}"
    # <additional code>  
  end
end

i=-1 before exception
Just executed 1/-1
i=0 before exception
Exception: divided by 0
Just executed 1/0

Notice that begin/rescue/ensure/end must be inside the loop and that the code after ensure is executed for each i regardless of whether a zero-divide exception occurs.

Upvotes: 0

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121020

TL;DR: There is no way to continue the execution of the method from inside the middle of it.

big_list.each do |i|
  # sensitive stuff
  result = begin
             sensitive_method(i)
             nil
           rescue Exception => e
             e
           end
  # other sensitive stuff
  case result
    when Interrupt
      puts "done"
      break "done"
    when Exception then raise result
  end
end

Sidenote: you probably don’t want to rescue the topmost Exception, but some subclass that makes sense to rescue.


To make it possible to finish the chunk of operations:

operations = [
  -> { puts "start" },
  -> { sleep 1 },
  -> { puts "halfway" },
  -> { sleep 1 },
  -> { puts "done\n\n" }
]

def safe_chunk(operations, index = 0)
  result = operations[index..-1].each_with_index(index) do |op, idx|
    begin
      op.()
    rescue Exception => e
      safe_chunk(operations, idx) # or idx + 1
      break e
    end
  end
  result.is_a?(Array) ? nil : result
end

Upvotes: 3

Related Questions