Class: Vagrant::BatchAction

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant/batch_action.rb

Overview

This class executes multiple actions as a single batch, parallelizing the action calls if possible.

Instance Method Summary collapse

Constructor Details

#initialize(allow_parallel = true) ⇒ BatchAction

Returns a new instance of BatchAction.


9
10
11
12
13
# File 'lib/vagrant/batch_action.rb', line 9

def initialize(allow_parallel=true)
  @actions          = []
  @allow_parallel   = allow_parallel
  @logger           = Log4r::Logger.new("vagrant::batch_action")
end

Instance Method Details

#action(machine, action, options = nil) ⇒ Object

Add an action to the batch of actions that will be run.

This will not run the action now. The action will be run when #run is called.

Parameters:

  • machine (Machine)

    The machine to run the action on

  • action (Symbol)

    The action to run

  • options (Hash) (defaults to: nil)

    Any additional options to send in.


23
24
25
# File 'lib/vagrant/batch_action.rb', line 23

def action(machine, action, options=nil)
  @actions << [machine, action, options]
end

#custom(machine, &block) ⇒ Object

Custom runs a custom proc against a machine.

Parameters:

  • machine (Machine)

    The machine to run against.


30
31
32
# File 'lib/vagrant/batch_action.rb', line 30

def custom(machine, &block)
  @actions << [machine, block, nil]
end

#runObject

Run all the queued up actions, parallelizing if possible.

This will parallelize if and only if the provider of every machine supports parallelization and parallelization is possible from initialization of the class.


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/vagrant/batch_action.rb', line 39

def run
  par = false

  if @allow_parallel
    par = true
    @logger.info("Enabling parallelization by default.")
  end

  if par
    @actions.each do |machine, _, _|
      if !machine.provider_options[:parallel]
        @logger.info("Disabling parallelization because provider doesn't support it: #{machine.provider_name}")
        par = false
        break
      end
    end
  end

  if par && @actions.length <= 1
    @logger.info("Disabling parallelization because only executing one action")
    par = false
  end

  @logger.info("Batch action will parallelize: #{par.inspect}")

  threads = []
  @actions.each do |machine, action, options|
    @logger.info("Starting action: #{machine} #{action} #{options}")

    # Create the new thread to run our action. This is basically just
    # calling the action but also contains some error handling in it
    # as well.
    thread = Thread.new do
      Thread.current[:error] = nil

      # Note that this thread is being used for running
      # a batch action
      Thread.current[:batch_parallel_action] = par

      # Record our pid when we started in order to figure out if
      # we've forked...
      start_pid = Process.pid

      begin
        if action.is_a?(Proc)
          action.call(machine)
        else
          machine.send(:action, action, options)
        end
      rescue Exception => e
        # If we're not parallelizing, then raise the error. We also
        # don't raise the error if we've forked, because it'll hang
        # the process.
        raise if !par && Process.pid == start_pid

        # Store the exception that will be processed later
        Thread.current[:error] = e

        # We can only do the things below if we do not fork, otherwise
        # it'll hang the process.
        if Process.pid == start_pid
          # Let the user know that this process had an error early
          # so that they see it while other things are happening.
          machine.ui.error(I18n.t("vagrant.general.batch_notify_error"))
        end
      end

      # If we forked during the process run, we need to do a hard
      # exit here. Ruby's fork only copies the running process (which
      # would be us), so if we return from this thread, it results
      # in a zombie Ruby process.
      if Process.pid != start_pid
        # We forked.

        exit_status = true
        if Thread.current[:error]
          # We had an error, print the stack trace and exit immediately.
          exit_status = false
          error = Thread.current[:error]
          @logger.error(error.inspect)
          @logger.error(error.message)
          @logger.error(error.backtrace.join("\n"))
        end

        Process.exit!(exit_status)
      end
    end

    # Set some attributes on the thread for later
    thread[:machine] = machine

    if !par
      thread.join(THREAD_MAX_JOIN_TIMEOUT) while thread.alive?
    end
    threads << thread
  end

  errors = []

  threads.each do |thread|
    # Wait for the thread to complete
    thread.join(THREAD_MAX_JOIN_TIMEOUT) while thread.alive?

    # If the thread had an error, then store the error to show later
    if thread[:error]
      e = thread[:error]
      # If the error isn't a Vagrant error, then store the backtrace
      # as well.
      if !thread[:error].is_a?(Errors::VagrantError)
        e       = thread[:error]
        message = e.message
        message += "\n"
        message += "\n#{e.backtrace.join("\n")}"

        errors << I18n.t("vagrant.general.batch_unexpected_error",
                         machine: thread[:machine].name,
                         message: message)
      else
        errors << I18n.t("vagrant.general.batch_vagrant_error",
                         machine: thread[:machine].name,
                         message: thread[:error].message)
      end
    end
  end

  if !errors.empty?
    raise Errors::BatchMultiError, message: errors.join("\n\n")
  end

  # Check if any threads set an exit code and exit if found. If
  # multiple threads have exit code values set, the first encountered
  # will be the value used.
  threads.each do |thread|
    if thread[:exit_code]
      @logger.debug("Found exit code set within batch action thread. Exiting")
      Process.exit!(thread[:exit_code])
    end
  end
end