Class: Bindan::Emulator::FirestoreController

Inherits:
Object
  • Object
show all
Defined in:
lib/bindan/emulators/firestore_controller.rb

Overview

Represents and controls a single instance of the Google Cloud Firestore emulator process. This class encapsulates the logic for starting, stopping, and waiting for the emulator, separating process management from the test execution flow.

Constant Summary collapse

WAIT_TIMEOUT =

seconds

15
INITIAL_BACKOFF_KEY_SEC =

seconds

0.1
BACKOFF_MULTIPLIER =
1.5

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pid, host, port) ⇒ FirestoreController

Returns a new instance of FirestoreController.

Parameters:

  • pid (Integer) —
    • process group id


108
109
110
111
112
# File 'lib/bindan/emulators/firestore_controller.rb', line 108

def initialize(pid, host, port)
  @pid = pid
  @host = host
  @port = port
end

Class Method Details

.host_and_port(host = nil, port = nil) ⇒ Array<String, Integer>

Returns:

  • (Array<String, Integer>)


24
25
26
27
28
29
30
31
# File 'lib/bindan/emulators/firestore_controller.rb', line 24

def host_and_port(host = nil, port = nil)
  host, port = ENV["FIRESTORE_EMULATOR_HOST"].to_s.split(":") if ENV["FIRESTORE_EMULATOR_HOST"]

  host ||= ENV["CI"] ? "0.0.0.0" : "localhost"
  port ||= 8080

  [host, port]
end

.kill_process_if_already_exists(host:, port:, with_message: true) ⇒ Object

Parameters:

  • host (Integer)
  • port (Integer)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/bindan/emulators/firestore_controller.rb', line 88

def kill_process_if_already_exists(host:, port:, with_message: true)
  TCPSocket.new(host, port).close

  # `lsof -Fg -i:PORT` returns PIDs of the process listening on the port.
  process_ids, = Open3.capture3(*"lsof -Fg -g -s TCP:LISTEN -i :#{port}".split(" "))
  process_group = process_ids.lines.map(&:chomp).find { |id| id =~ /^g([0-9]+)/ }

  if process_group
    id = process_group[1..].to_i # strip first letter
    puts "   Found emulator process group #{id} on port #{port}. Terminating." if with_message
    stop(id, with_message: false)
  end
rescue Errno::ECONNREFUSED
  # noop
end

.start(import: nil, export: nil, host: nil, port: nil, close_io: true) ⇒ FirestoreEmulatorController

Initiates the emulator process and returns a new instance.

Parameters:

  • import (String) (defaults to: nil)
  • export (String) (defaults to: nil)
  • host (String) (defaults to: nil)
  • port (Integer) (defaults to: nil)
  • close_io (bool) (defaults to: true)

Returns:

  • (FirestoreEmulatorController) —

    An instance to manage the emulator lifecycle.

Raises:

  • (Errno)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/bindan/emulators/firestore_controller.rb', line 44

def start(import: nil, export: nil, host: nil, port: nil, close_io: true)
  host, port = host_and_port(host, port)

  puts " Starting Firestore emulator ..." unless close_io
  kill_process_if_already_exists(host: host, port: port, with_message: !close_io)

  #
  # This command is known to launch a background Java process and then exit.
  # Trying keep process info with process group
  #
  cmd = "gcloud emulators firestore start --host-port=#{host}:#{port}"
  cmd += " --import-data=#{import}" if import
  cmd += " --export-on-exit=#{export}" if export
  opts = close_io ? {out: "/dev/null", err: "/dev/null"} : {}
  pid = Process.spawn(*cmd, pgroup: true, **opts) # steep:ignore

  puts "   Emulator process initiated." unless close_io
  new(pid, host, port)
end

.stop(pid, with_message: true) ⇒ void

This method returns an undefined value.

kill process group

Parameters:

  • pid (Integer)
  • with_message (bool) (defaults to: true)


71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/bindan/emulators/firestore_controller.rb', line 71

def stop(pid, with_message: true)
  puts "\n=> Stopping Firestore emulator..." if with_message

  begin
    Process.kill("TERM", -pid)
    Process.wait(-pid)
  rescue Errno::ESRCH, Errno::ECHILD
    # Process was already killed or doesn't exist, which is fine.
  end

  puts "   Emulator process terminated." if with_message
end

Instance Method Details

#stop(with_message: true) ⇒ void

This method returns an undefined value.



147
148
149
# File 'lib/bindan/emulators/firestore_controller.rb', line 147

def stop(with_message: true)
  self.class.stop(@pid, with_message: with_message)
end

#wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER, with_message: true) ⇒ void

This method returns an undefined value.

waits to accessable

Parameters:

  • timeout (number) (defaults to: WAIT_TIMEOUT)
  • backoff (number) (defaults to: INITIAL_BACKOFF_KEY_SEC)
  • multiplier (number) (defaults to: BACKOFF_MULTIPLIER)

Raises:

  • (Timeout::Error) —

    if the emulator does not become available within the configured timeout.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/bindan/emulators/firestore_controller.rb', line 123

def wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER, with_message: true)
  last_rescue_error = nil

  puts "   Waiting for emulator to be ready on #{@host}:#{@port}..." if with_message
  count = 1
  Timeout.timeout(timeout) do
    loop do
      TCPSocket.new(@host, @port).close
      puts "   Emulator is up and listening." if with_message
      return
    rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL => e
      last_rescue_error = e
      sleep backoff
      count += 1
      backoff *= multiplier**count
    end
  end
rescue Timeout::Error
  raise last_rescue_error # steep:ignore
end