Read more

Capybara: Preventing headless Chrome from freezing your test suite

Henning Koch
March 10, 2021Software engineer at makandra GmbH

We prefer to run our end-to-end tests with headless Chrome. While it's a very stable solution overall, we sometimes see the headless Chrome process freeze (or the Capybara driver losing connection, we're not sure).

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more Show archive.org snapshot

The effect is that your test suite suddenly stops progressing without an error. You will eventually see an error after a long timeout but until then it will seem that your suite is frozen. If you're also using capybara-screenshot Show archive.org snapshot you will sit out that timeout twice, as capybara-screen again tries to communicate with the dead Chrome process.

Workaround

The code below will address the issue in two ways:

  • It will close the Chrome process after groups of 50 tests, making the entire issue very unlikely. Capybara will automatically start a new process for the next test.
  • In case Chrome communication breaks down, it will immediately raise an error instead of waiting for a long timeout.

The code expands on work by Tobias Kraze Show archive.org snapshot .

Include the module

Include the following StabilizeHeadlessChrome module in your tests:

class StabilizeHeadlessChrome
  class Reaper
    MAX_SCENARIOS_PER_DRIVER = 50

    class << self

      def driver_scenario_count
        @driver_scenario_count || 0
      end

      attr_writer :driver_scenario_count

      def browser_died?
        !!@browser_died
      end

      attr_writer :browser_died

      def scenario_started
        self.browser_died = false
      end

      def scenario_ended
        return unless session.driver.is_a?(::Capybara::Selenium::Driver)

        self.driver_scenario_count += 1

        if !browser_died? && driver_scenario_count >= MAX_SCENARIOS_PER_DRIVER
          kill_session!
        end
      end

      def browser_died!
        self.browser_died = true
        kill_session!
      end

      def kill_session!
        self.driver_scenario_count = 0
        # This closes the browser
        session.quit
      end

      private

      def session
        ::Capybara.current_session
      end

    end
  end

  class CustomHttpClient < Selenium::WebDriver::Remote::Http::Default

    def initialize(open_timeout: 10, read_timeout: 60)
      super
    end

    private

    def response_for(request)
      if Reaper.browser_died?
        raise 'Waiting for session to restart'
      else
        super
      end
    rescue Net::OpenTimeout, Net::ReadTimeout
      # Chrome just died on us. We will reset the session, which causes Chrome to restart.
      # In this case, capturing a screenshot will not work for this scenario.
      warn "\n\e[31mKilling Capybara session!\e[0m\n"
      ::Reaper.browser_died!
      raise
    end
  end
end

Call the reaper

To restart the Chrome process after 50 tests, Now call StabilizeHeadlessChrome::Reaper when your test starts and ends.

E.g. in Cucumber:

Before do
  StabilizeHeadlessChrome::Reaper.scenario_started
end

After do
  StabilizeHeadlessChrome::Reaper.scenario_ended
end

Adjust driver registration

To get early errors (instead of long timeouts) you need to pass a new :http_client option when registering a new Capybara driver.

Your old driver registration looks like this:

Capybara.register_driver :selenium do |app|
  ...
  Capybara::Selenium::Driver.new(app,
    browser: :chrome,
    options: options
   )
end

It now needs to look like this:

Capybara.register_driver :selenium do |app|
  ...
  Capybara::Selenium::Driver.new(app,
    browser: :chrome,
    options: options,
    http_client: StabilizeHeadlessChrome::CustomHttpClient.new
   )
end
Henning Koch
March 10, 2021Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2021-03-10 17:05)