Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion lib/async/pool/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def start_gardener
end
ensure
@gardener = nil
self.close
self.terminate
end
end

Expand Down Expand Up @@ -351,6 +351,23 @@ def available_resource

private

# Force-retire all resources immediately, including busy ones, without waiting for them to be released. Unlike {drain}, this never blocks, so it is safe to call during cancellation when busy resources will never be released.
def terminate
# Re-read `@resources.first` each iteration: `retire` -> `resource.close` can yield, so the set of resources may change mid-teardown.
while (resource, _usage = @resources.first)
begin
retire(resource)
rescue => error
Console.warn(self, "Failed to retire resource during teardown!", resource: resource, exception: error)

# Ensure progress so the loop cannot spin forever on a resource that failed to retire:
@resources.delete(resource)
end
end

@available.clear
end

# Acquire an existing resource with zero usage.
# If there are resources that are in use, wait until they are released.
def acquire_existing_resource
Expand Down
53 changes: 53 additions & 0 deletions test/async/pool/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,59 @@

expect(events).to be == [:acquire, :close, :release, :closed]
end

it "does not deadlock when Sync scope exits with unreleased resources" do
thread = Thread.new do
Sync do
policy = proc{|pool| pool.prune(0)}
pool = subject.new(Async::Pool::Resource, policy: policy)

# Acquire a resource (starts the gardener) but don't release it
pool.acquire

# Let gardener start and enter its wait loop
sleep 0.05
end
end

result = thread.join(5)
thread.kill if result.nil?

expect(result).not.to be_nil
end

it "force-retires all resources even if closing one raises" do
closed = []

failing_resource = Class.new(Async::Pool::Resource) do
define_method(:close) do
closed << self
raise "Resource failed to close!"
end
end

thread = Thread.new do
Sync do
policy = proc{|pool| pool.prune(0)}
pool = subject.new(failing_resource, policy: policy)

# Acquire two resources (concurrency 1 each) but don't release them:
pool.acquire
pool.acquire

# Let the gardener start and enter its wait loop:
sleep 0.05
end
end

result = thread.join(5)
thread.kill if result.nil?

# Teardown must force-retire every resource, even though closing the
# first one raises:
expect(result).not.to be_nil
expect(closed.size).to be == 2
end
end

with "#to_s" do
Expand Down
Loading