-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathfuture-resource.rb
More file actions
136 lines (123 loc) · 3.53 KB
/
Copy pathfuture-resource.rb
File metadata and controls
136 lines (123 loc) · 3.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
require "thread"
require "monitor"
require "timeout"
##
# future-resource allows you to wait on a final value being set for a placeholder, which may occur asynchronously.
#
# @author Ben Langfeld
# @author Jay Phillips
#
# @example false printed first, followed by a delay before :foo is printed
# fr = FutureResource.new
#
# Thread.new do
# sleep 10
# fr.resource = :foo
# end
#
# p fr.set_yet?
# p fr.resource
#
class FutureResource
NULL = Object.new
private_constant :NULL
##
# Create a new FutureResource.
#
def initialize(blocker = nil)
@resource_lock = Monitor.new
@resource_value_blocker = blocker || @resource_lock.new_cond
@terminated = false
@resource = NULL
end
##
# Checks if the value of the resource placeholder has been set yet.
#
# @return [Boolean]
#
def set_yet?
@resource_lock.synchronize { ! @resource.equal?(NULL) }
end
##
# Checks if the attempt to read the resource has been terminated.
#
# @return [Boolean]
#
def terminated?
@resource_lock.synchronize { @terminated }
end
##
# Returns the value of a specific resource, optionally waiting for `timeout` seconds before
# raising a Timeout::Error exception, or raising a FutureResource::Terminated exception if
# the attempt to read the resource is terminated early by another thread. When called on
# an unset resource without a timeout, raises a deadlock.
#
# @param [Integer] timeout number of seconds to wait for the resource to become ready
#
# @raise [Timeout::Error] if timeout expires and resource is not ready
# @raise [FutureResource::Terminated] if the attempt to read the resource is terminated by another thread
#
# @return [Object]
#
def resource(timeout = nil)
@resource_lock.synchronize do
@resource_value_blocker.wait(timeout) unless set_yet? || terminated?
raise Terminated if terminated?
raise Timeout.new(timeout.to_s) if @resource.equal?(NULL)
@resource
end
end
##
# Sets the value for the resource, making it available for all waiting and following reads.
# Resource values can only be set once. Calling this method on a terminated resource is
# ineffective.
#
# @param [Object] resource any value to be set for the resource
#
# @raise [FutureResource::ResourceAlreadySet] if resource is already set
#
def resource=(resource)
set_or_terminate do
@resource = resource
end
end
##
# Terminates the attempt to read the resource early, causing those waiting and any
# following reads to raise a FutureResource::Terminated exception. Subsequently calling
# this method again is ineffective.
#
# @raise [FutureResource::ResourceAlreadySet] if resource is already set
#
def terminate
set_or_terminate do
@terminated = true
end
end
##
# Raised when the program tries to set a value for the resource that is already set.
#
class ResourceAlreadySetException < StandardError
def initialize
super "Cannot set this resource twice!"
end
end
##
# Raised when the attempt to read the resource is terminated early.
#
class Terminated < StandardError
def initialize
super "Resource read attempt terminated"
end
end
Timeout = ::Timeout::Error
private
def set_or_terminate
@resource_lock.synchronize do
return if terminated?
raise ResourceAlreadySetException if set_yet?
yield
@resource_value_blocker.broadcast
@resource_value_blocker = nil # Don't really need it anymore.
end
end
end