-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add access to batches of tests fetched from the Queue API in RSpec, Minitest, Cucumber #252
Changes from all commits
a2959e8
b064356
e33c5d2
a3a9a84
ba2b4f5
4076cbf
620de30
2e5efb0
a86bb07
fb9bc6d
4c4b0c4
a9819b8
75679a7
93acf24
61690b5
799ae18
a613b92
f2b51d1
0cf2ce3
4ef38a4
756ac6b
0b06ea0
c199f4e
03e218e
b8d86d0
2ef74eb
550a7f1
94fa4ef
ef860f1
e9d83ef
c67c394
c305c53
9b430aa
1e12f60
04eced6
c0d9dde
f9d58c7
b8d19a3
00b37f0
4cd527a
0bf51d9
03c15ea
a7dc8ba
bb371e8
b977b3b
b819328
d200ad9
183f4ef
f027d8a
e474b49
24cd850
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# frozen_string_literal: true | ||
|
||
module KnapsackPro | ||
module Store | ||
# Consumers of this class are the gem's users. | ||
# Ensure the API is backward compatible when introducing changes. | ||
class Client | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I much prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the Store still be a module or should I convert it to a class? |
||
def self.batches | ||
client.batches | ||
end | ||
|
||
private | ||
|
||
def self.client | ||
KnapsackPro::Store::Server.client | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# frozen_string_literal: true | ||
|
||
module KnapsackPro | ||
module Store | ||
class QueueBatchManager | ||
attr_reader :batches | ||
|
||
def initialize | ||
@batches = [] | ||
end | ||
|
||
def add_batch(test_file_paths) | ||
return if test_file_paths.empty? | ||
@batches << KnapsackPro::Store::TestBatch.new(test_file_paths) | ||
end | ||
|
||
def last_batch_passed! | ||
@batches.last.send(:passed!) | ||
end | ||
|
||
def last_batch_failed! | ||
@batches.last.send(:failed!) | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# frozen_string_literal: true | ||
|
||
module KnapsackPro | ||
module Store | ||
class Server | ||
extend Forwardable | ||
|
||
def self.start | ||
return unless @server_pid.nil? | ||
|
||
assign_available_store_server_uri | ||
|
||
@server_pid = fork do | ||
queue = Thread::Queue.new | ||
|
||
Signal.trap("TERM") { | ||
queue.push(nil) | ||
} | ||
|
||
DRb.start_service(store_server_uri, new) | ||
queue.pop | ||
DRb.stop_service | ||
|
||
# Wait for the drb server thread to finish before exiting. | ||
DRb.thread&.join | ||
end | ||
|
||
::Kernel.at_exit do | ||
stop | ||
end | ||
end | ||
|
||
def self.client | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This returns the object served by the server. But outside of this file, nobody knows there's a server/client. What if we call it |
||
return @client unless @client.nil? | ||
|
||
# must be called at least once per process | ||
# https://ruby-doc.org/stdlib-2.7.0/libdoc/drb/rdoc/DRb.html | ||
DRb.start_service | ||
|
||
@client = DRbObject.new_with_uri(store_server_uri) | ||
|
||
begin | ||
retries ||= 0 | ||
@client.ping | ||
@client | ||
rescue DRb::DRbConnError | ||
wait_seconds = 0.1 | ||
retries += wait_seconds | ||
sleep wait_seconds | ||
retry if retries <= 3 # seconds | ||
raise | ||
end | ||
end | ||
|
||
def_delegators :@queue_batch_manager, :add_batch, :last_batch_passed!, :last_batch_failed!, :batches | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could, but I wanted to have a single DRb serivce (store) in a long term when we add more features to it (perhaps we would store data in memory instead of using json files). I prefer that instead of creating a new DRb service per feature. I use the following architecture:
|
||
|
||
def initialize | ||
@queue_batch_manager = KnapsackPro::Store::QueueBatchManager.new | ||
end | ||
|
||
def ping | ||
true | ||
end | ||
3v0k4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private | ||
|
||
def self.stop | ||
return if @server_pid.nil? | ||
Process.kill('TERM', @server_pid) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could raise if the pid is not found. Is there any chance it could happen that pid is not a running process? Maybe because of multiple calls to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. I wanted our tests (
I hope not. I tested that. It is not possible to kill a child process (the store which is forked process) even with KILL -9. The parent process must be shutdown first. The parent would terminate the child process (the store). Just in case I handle scenario when the PID does not exist and |
||
Process.waitpid2(@server_pid) | ||
3v0k4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@server_pid = nil | ||
rescue Errno::ESRCH # process does not exist | ||
@server_pid = nil | ||
end | ||
|
||
def self.set_store_server_uri(uri) | ||
ENV['KNAPSACK_PRO_STORE_SERVER_URI'] = uri | ||
3v0k4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
def self.store_server_uri | ||
ENV['KNAPSACK_PRO_STORE_SERVER_URI'] || raise("KNAPSACK_PRO_STORE_SERVER_URI must be set to available DRb port.") | ||
end | ||
|
||
# must be set in the main/parent process to make the env var available to the child process | ||
def self.assign_available_store_server_uri | ||
@assigned_store_server_uri ||= | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed the code to d200ad9 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reverted it to the original code because it was causing flaky tests. |
||
begin | ||
find_available_drb_port_for_dummy_service | ||
set_store_server_uri(DRb.uri) | ||
stop_dummy_service | ||
true | ||
end | ||
end | ||
|
||
def self.find_available_drb_port_for_dummy_service | ||
DRb.start_service('druby://localhost:0') | ||
end | ||
|
||
def self.stop_dummy_service | ||
DRb.stop_service | ||
end | ||
end | ||
|
||
class TestableServer < Server | ||
def self.assign_available_store_server_uri | ||
super | ||
end | ||
|
||
def self.reset | ||
stop | ||
set_store_server_uri(nil) | ||
@assigned_store_server_uri = nil | ||
@client = nil | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# frozen_string_literal: true | ||
|
||
module KnapsackPro | ||
module Store | ||
class TestBatch | ||
BatchNotExecutedError = Class.new(StandardError) | ||
|
||
attr_reader :test_file_paths | ||
|
||
def initialize(test_file_paths) | ||
@test_file_paths = test_file_paths | ||
@passed = nil | ||
end | ||
|
||
def executed? | ||
!@passed.nil? | ||
end | ||
|
||
def passed? | ||
raise BatchNotExecutedError.new unless executed? | ||
return @passed | ||
end | ||
|
||
private | ||
|
||
def passed! | ||
@passed = true | ||
end | ||
|
||
def failed! | ||
@passed = false | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe let's add a link to the docs when it's documented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to add an exact example of how to use the new feature in the changelog. The changelog would remain accurate for a given version of the knapsack_pro gem if docs change in the future.