quack_concurrency 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1e1b4dcf29faab53b77b05eb8017b8b73c339829
4
- data.tar.gz: 85e7852afdcad9dba9fab2b264104844ebd54ad9
2
+ SHA256:
3
+ metadata.gz: 164e219c34134cf836d5a4169272a1f6185d832790ae69c29ad4dd4496a2af0f
4
+ data.tar.gz: e4f01597b27e39da0e1d916228abe30764b5b8b30a3d68d90fabea53635a5d10
5
5
  SHA512:
6
- metadata.gz: b934eb948f2cb2a9a7565db507d83130e14363b3ab2e816f590f06ef8d9ccdeff8c3f6e655e4dcf9878f77f1311fcb0ff95ee092cdd881c26ea6e66840cb7c1c
7
- data.tar.gz: 27891264874dd9a5083dcb7e9ac10a18665eeb7d538b10caa6471e69bda84c3c93996b3f9d49e4b602f70fe94593a41edb001c77e46b38deb6dad77ec75dee84
6
+ metadata.gz: 94ffaae9048528509d569619625ce1288ac16de3015f70fd92f08f52309ee3c15f91b07f1d1eb5bb5fb3b4ab6edd5022cd919c84f9a98672b04adb2e91d40686
7
+ data.tar.gz: 26da3552b5b6b17d4f60f0427f7fa9a1b081edeab9df71cd7760c236e5edb69bad1a491d3185aa91e79329a688279ac21062fc0882566f927f1cb9b35850dc6e
@@ -0,0 +1,26 @@
1
+ module QuackConcurrency
2
+ class ConcurrencyTool
3
+
4
+ def setup_duck_types(supplied_classes)
5
+ resultant_classes = {}
6
+ required_classes = [:condition_variable, :mutex]
7
+ required_classes = required_classes.map { |name| Name.new(name.to_s) }
8
+ if supplied_classes
9
+ raise ArgumentError, "'supplied_classes' must be Hash" unless supplied_classes.is_a?(Hash)
10
+ supplied_classes = supplied_classes.map { |k, v| [Name.new(k.to_s), v] }.to_h
11
+ required_classes.each do |class_name|
12
+ unless supplied_classes[class_name]
13
+ raise ArgumentError, "missing duck type: #{class_name.camel_case(:upper)}"
14
+ end
15
+ resultant_classes[class_name.snake_case.to_sym] = supplied_classes[class_name]
16
+ end
17
+ else
18
+ required_classes.each do |class_name|
19
+ resultant_classes[class_name.snake_case.to_sym] = Object.const_get(class_name.camel_case(:upper))
20
+ end
21
+ end
22
+ resultant_classes
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ module QuackConcurrency
2
+ class Error < StandardError
3
+ end
4
+ end
5
+
@@ -0,0 +1,6 @@
1
+ module QuackConcurrency
2
+ class Future
3
+ class Canceled < Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module QuackConcurrency
2
+ class Future
3
+ class Complete < Error
4
+ end
5
+ end
6
+ end
@@ -1,14 +1,10 @@
1
1
  module QuackConcurrency
2
- class Future
3
-
4
- class Canceled < StandardError
5
- end
2
+ class Future < ConcurrencyTool
6
3
 
7
- def initialize(duck_types: {})
8
- condition_variable_class = duck_types[:condition_variable] || ConditionVariable
9
- mutex_class = duck_types[:mutex] || Mutex
10
- @condition_variable = condition_variable_class.new
11
- @mutex = mutex_class.new
4
+ def initialize(duck_types: nil)
5
+ classes = setup_duck_types(duck_types)
6
+ @condition_variable = classes[:condition_variable].new
7
+ @mutex = classes[:mutex].new
12
8
  @value = nil
13
9
  @value_set = false
14
10
  @complete = false
@@ -16,18 +12,19 @@ module QuackConcurrency
16
12
 
17
13
  def set(new_value = nil)
18
14
  @mutex.synchronize do
19
- raise if @complete
15
+ raise Complete if @complete
20
16
  @value_set = true
21
17
  @complete = true
22
18
  @value = new_value
23
19
  @condition_variable.broadcast
24
20
  end
21
+ nil
25
22
  end
26
23
 
27
24
  def get
28
25
  @mutex.synchronize do
29
26
  @condition_variable.wait(@mutex) unless complete?
30
- raise 'should not get here' unless complete?
27
+ raise 'internal error, invalid state' unless complete?
31
28
  raise Canceled unless @value_set
32
29
  @value
33
30
  end
@@ -35,9 +32,11 @@ module QuackConcurrency
35
32
 
36
33
  def cancel
37
34
  @mutex.synchronize do
38
- raise if @complete
35
+ raise Complete if @complete
39
36
  @complete = true
37
+ @condition_variable.broadcast
40
38
  end
39
+ nil
41
40
  end
42
41
 
43
42
  def complete?
@@ -0,0 +1,31 @@
1
+ module QuackConcurrency
2
+ class Name < String
3
+
4
+ def initialize(*args)
5
+ super
6
+ # set all names to a default case
7
+ # if we create two of the same name with different cases they will now be equal
8
+ replace(snake_case)
9
+ end
10
+
11
+ def camel_case(first_letter = :upper)
12
+ case first_letter
13
+ when :upper
14
+ self.split('_').collect(&:capitalize).join
15
+ when :lower
16
+ self.camelcase(:upper)[0].downcase + self.camelcase(:upper)[1..-1]
17
+ else
18
+ raise ArgumentError, 'invalid option, use either :upper or :lower'
19
+ end
20
+ end
21
+
22
+ def snake_case
23
+ self.gsub(/::/, '/').
24
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
25
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
26
+ tr("- ", "_").
27
+ downcase
28
+ end
29
+
30
+ end
31
+ end
@@ -1,11 +1,10 @@
1
1
  module QuackConcurrency
2
- class Queue
2
+ class Queue < ConcurrencyTool
3
3
 
4
- def initialize(duck_types: {})
5
- condition_variable_class = duck_types[:condition_variable] || ConditionVariable
6
- mutex_class = duck_types[:mutex] || Mutex
7
- @condition_variable = condition_variable_class.new
8
- @mutex = mutex_class.new
4
+ def initialize(duck_types: nil)
5
+ classes = setup_duck_types(duck_types)
6
+ @condition_variable = classes[:condition_variable].new
7
+ @mutex = classes[:mutex].new
9
8
  @queue = []
10
9
  @waiting_count = 0
11
10
  @closed = false
@@ -0,0 +1,6 @@
1
+ module QuackConcurrency
2
+ class ReentrantMutex
3
+ class Error < Error
4
+ end
5
+ end
6
+ end
@@ -1,14 +1,12 @@
1
1
  # based off https://en.wikipedia.org/wiki/Reentrant_mutex
2
2
 
3
3
  module QuackConcurrency
4
- class ReentrantMutex
4
+ class ReentrantMutex < ConcurrencyTool
5
5
 
6
- def initialize(duck_types: {})
7
- condition_variable_class = duck_types[:condition_variable] || ConditionVariable
8
- @kernel_module = duck_types[:kernel] || Kernel
9
- mutex_class = duck_types[:mutex] || Mutex
10
- @condition_variable = condition_variable_class.new
11
- @mutex = mutex_class.new
6
+ def initialize(duck_types: nil)
7
+ classes = setup_duck_types(duck_types)
8
+ @condition_variable = classes[:condition_variable].new
9
+ @mutex = classes[:mutex].new
12
10
  @owner = nil
13
11
  @lock_depth = 0
14
12
  end
@@ -16,6 +14,7 @@ module QuackConcurrency
16
14
  def lock
17
15
  @mutex.synchronize do
18
16
  @condition_variable.wait(@mutex) if @owner && @owner != caller
17
+ raise 'internal error, invalid state' if @owner && @owner != caller
19
18
  @owner = caller
20
19
  @lock_depth += 1
21
20
  end
@@ -34,14 +33,16 @@ module QuackConcurrency
34
33
  @owner == caller
35
34
  end
36
35
 
37
- def sleep(timeout = nil)
36
+ def sleep(*args)
38
37
  unlock
39
- if timeout
40
- @kernel_module.sleep(timeout)
41
- else
42
- @kernel_module.sleep
38
+ # i would rather not need to get a ducktype for sleep so we will just take
39
+ # advantage of Mutex's sleep method that must take it into account already
40
+ @mutex.synchronize do
41
+ @mutex.sleep(*args)
43
42
  end
44
- lock
43
+ nil
44
+ ensure
45
+ lock unless owned?
45
46
  end
46
47
 
47
48
  def synchronize
@@ -52,27 +53,24 @@ module QuackConcurrency
52
53
  result
53
54
  ensure
54
55
  unless @lock_depth == start_depth && @owner == start_owner
55
- raise 'could not unlock mutex as its state has been modified'
56
+ raise Error, 'could not unlock reentrant mutex as its state has been modified'
56
57
  end
57
58
  unlock
58
59
  end
59
60
 
60
61
  def try_lock
61
62
  @mutex.synchronize do
62
- if @owner && @owner != caller
63
- return false
64
- else
65
- @owner = caller
66
- @lock_depth += 1
67
- end
63
+ return false if @owner && @owner != caller
64
+ @owner = caller
65
+ @lock_depth += 1
66
+ true
68
67
  end
69
- true
70
68
  end
71
69
 
72
70
  def unlock
73
71
  @mutex.synchronize do
74
- raise "not locked" if @lock_depth == 0
75
- raise "not the owner" unless @owner == caller
72
+ raise Error, 'can not unlock reentrant mutex, it is not locked' if @lock_depth == 0
73
+ raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless @owner == caller
76
74
  @lock_depth -= 1
77
75
  if @lock_depth == 0
78
76
  @owner = nil
@@ -0,0 +1,6 @@
1
+ module QuackConcurrency
2
+ class Semaphore
3
+ class Error < Error
4
+ end
5
+ end
6
+ end
@@ -1,30 +1,52 @@
1
1
  module QuackConcurrency
2
- class Semaphore
2
+ class Semaphore < ConcurrencyTool
3
3
 
4
4
  attr_reader :permit_count
5
5
 
6
- def initialize(permit_count = 1, duck_types: {})
7
- condition_variable_class = duck_types[:condition_variable] || ConditionVariable
8
- raise 'permit_count invalid' if permit_count < 1
6
+ def initialize(permit_count = 1, duck_types: nil)
7
+ classes = setup_duck_types(duck_types)
8
+ @condition_variable = classes[:condition_variable].new
9
+ verify_permit_count(permit_count)
9
10
  @permit_count = permit_count
10
11
  @permits_used = 0
11
- @condition_variable = condition_variable_class.new
12
12
  @mutex = ReentrantMutex.new(duck_types: duck_types)
13
13
  end
14
14
 
15
- def acquire
15
+ def permit_available?
16
+ permits_available >= 1
17
+ end
18
+
19
+ def permits_available
20
+ @mutex.synchronize do
21
+ raw_permits_available = @permit_count - @permits_used
22
+ raw_permits_available.positive? ? raw_permits_available : 0
23
+ end
24
+ end
25
+
26
+ def reacquire
27
+ @mutex.synchronize do
28
+ raise Error, 'can not reacquire a permit, no permits released right now' if @permits_used == 0
29
+ @permits_used -= 1
30
+ @condition_variable.signal if permit_available?
31
+ end
32
+ nil
33
+ end
34
+
35
+ def release
16
36
  @mutex.synchronize do
17
37
  @condition_variable.wait(@mutex) unless permit_available?
38
+ raise 'internal error, invalid state' unless permit_available?
18
39
  @permits_used += 1
19
40
  end
20
41
  nil
21
42
  end
22
43
 
23
44
  def set_permit_count(new_permit_count)
45
+ verify_permit_count(new_permit_count)
24
46
  @mutex.synchronize do
25
47
  remove_permits = @permit_count - new_permit_count
26
48
  if remove_permits.positive? && remove_permits > permits_available
27
- raise 'can not remove enough permits right not'
49
+ raise Error, 'can not set new permit count, not enough permits available to remove right now'
28
50
  end
29
51
  set_permit_count!(new_permit_count)
30
52
  end
@@ -32,7 +54,7 @@ module QuackConcurrency
32
54
  end
33
55
 
34
56
  def set_permit_count!(new_permit_count)
35
- raise "'permit_count' invalid" if new_permit_count < 1
57
+ verify_permit_count(new_permit_count)
36
58
  @mutex.synchronize do
37
59
  new_permits = new_permit_count - @permit_count
38
60
  if new_permits.positive?
@@ -45,42 +67,44 @@ module QuackConcurrency
45
67
  nil
46
68
  end
47
69
 
48
- def release
49
- @mutex.synchronize do
50
- raise 'no pemit to release' if @permits_used == 0
51
- @permits_used -= 1
52
- @condition_variable.signal if permit_available?
70
+ def synchronize
71
+ release
72
+ begin
73
+ yield
74
+ ensure
75
+ reacquire
53
76
  end
54
- nil
55
77
  end
56
78
 
57
- # how to handle if yield raises an error but has temporarily released its permit?
58
- #def synchronize
59
- # acquire
60
- # begin
61
- # yield
62
- # ensure
63
- # release
64
- # end
65
- #end
66
-
67
- def permits_available
68
- @mutex.synchronize { @permit_count - @permits_used }
69
- end
70
-
71
- def permit_available?
72
- @mutex.synchronize { permits_available >= 1 }
79
+ def try_release
80
+ @mutex.synchronize do
81
+ if permit_available?
82
+ release
83
+ true
84
+ else
85
+ false
86
+ end
87
+ end
73
88
  end
74
89
 
75
90
  private
76
91
 
77
92
  def add_permit
78
93
  @permit_count += 1
79
- @condition_variable.signal
94
+ @condition_variable.signal if permit_available?
95
+ nil
80
96
  end
81
97
 
82
98
  def remove_permit!
83
99
  @permit_count -= 1
100
+ raise 'internal error, invalid state' if @permit_count < 0
101
+ nil
102
+ end
103
+
104
+ def verify_permit_count(permit_count)
105
+ unless permit_count.is_a?(Integer) && permit_count >= 0
106
+ raise ArgumentError, "'permit_count' must be a non negative Integer"
107
+ end
84
108
  end
85
109
 
86
110
  end
@@ -1,7 +1,7 @@
1
1
  module QuackConcurrency
2
- class Waiter
2
+ class Waiter < ConcurrencyTool
3
3
 
4
- def initialize(duck_types: {})
4
+ def initialize(duck_types: nil)
5
5
  @queue = Queue.new(duck_types: duck_types)
6
6
  end
7
7
 
@@ -1,11 +1,27 @@
1
1
  require 'thread'
2
2
 
3
+ require 'quack_concurrency/concurrency_tool'
4
+ require 'quack_concurrency/error'
3
5
  require 'quack_concurrency/future'
6
+ require 'quack_concurrency/name'
4
7
  require 'quack_concurrency/queue'
5
8
  require 'quack_concurrency/reentrant_mutex'
6
9
  require 'quack_concurrency/semaphore'
7
10
  require 'quack_concurrency/waiter'
11
+ require 'quack_concurrency/future/canceled'
12
+ require 'quack_concurrency/future/complete'
13
+ require 'quack_concurrency/reentrant_mutex/error'
14
+ require 'quack_concurrency/semaphore/error'
15
+
16
+
17
+ # if you pass a duck type Hash to any of the concurrency tools it will force you to
18
+ # supply all the required ducktypes, all or nothing, as it were
19
+ # this is to protect against forgetting to pass one of the duck types as this
20
+ # would be a hard bug to solve otherwise
8
21
 
9
22
 
10
23
  module QuackConcurrency
24
+
25
+ ClosedQueueError = ::ClosedQueueError
26
+
11
27
  end
@@ -0,0 +1,145 @@
1
+ require 'quack_concurrency'
2
+
3
+ RSpec.describe QuackConcurrency::Future do
4
+
5
+ describe "::new" do
6
+
7
+ context "when called without a 'duck_types' argument" do
8
+ it "should create a new QuackConcurrency::Future" do
9
+ future = QuackConcurrency::Future.new
10
+ expect(future).to be_a(QuackConcurrency::Future)
11
+ end
12
+ end
13
+
14
+ context "when called with 'condition_variable' and 'mutex' duck types" do
15
+ it "should create a new QuackConcurrency::Future" do
16
+ duck_types = {condition_variable: Class.new, mutex: Class.new}
17
+ future = QuackConcurrency::Future.new(duck_types: duck_types)
18
+ expect(future).to be_a(QuackConcurrency::Future)
19
+ end
20
+ end
21
+
22
+ context "when called with only 'condition_variable' duck type" do
23
+ it "should raise ArgumentError" do
24
+ duck_types = {condition_variable: Class.new}
25
+ expect{ QuackConcurrency::Future.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+
29
+ context "when called with only 'mutex' duck type" do
30
+ it "should raise ArgumentError" do
31
+ duck_types = {mutex: Class.new}
32
+ expect{ QuackConcurrency::Future.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ describe "#set" do
39
+
40
+ context "when called" do
41
+ it "should not raise error" do
42
+ future = QuackConcurrency::Future.new
43
+ expect{ future.set(1) }.not_to raise_error
44
+ end
45
+ end
46
+
47
+ context "when called a second time" do
48
+ it "should raise QuackConcurrency::Future::Complete" do
49
+ future = QuackConcurrency::Future.new
50
+ future.set(1)
51
+ expect{ future.set(2) }.to raise_error(QuackConcurrency::Future::Complete)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ describe "#cancel" do
58
+
59
+ context "when called" do
60
+ it "should not raise error" do
61
+ future = QuackConcurrency::Future.new
62
+ expect{ future.cancel }.not_to raise_error
63
+ end
64
+ end
65
+
66
+ context "when called a second time" do
67
+ it "should raise QuackConcurrency::Future::Complete" do
68
+ future = QuackConcurrency::Future.new
69
+ future.cancel
70
+ expect{ future.cancel }.to raise_error(QuackConcurrency::Future::Complete)
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ describe "#set, #get" do
77
+
78
+ context "when #get called after #set" do
79
+ it "should return value from #set argument" do
80
+ future = QuackConcurrency::Future.new
81
+ future.set(1)
82
+ expect(future.get).to eql 1
83
+ end
84
+ end
85
+
86
+ context "when #get called a second time" do
87
+ it "should return value from #set argument" do
88
+ future = QuackConcurrency::Future.new
89
+ future.set(1)
90
+ future.get
91
+ expect(future.get).to eql 1
92
+ end
93
+ end
94
+
95
+ context "when #get called before #set" do
96
+ it "should wait and return value from #set argument after #set is called" do
97
+ future = QuackConcurrency::Future.new
98
+ thread = Thread.new do
99
+ sleep 1
100
+ future.set 1
101
+ end
102
+ start_time = Time.now
103
+ expect(future.get).to eql 1
104
+ end_time = Time.now
105
+ duration = end_time - start_time
106
+ thread.join
107
+ expect(duration).to be > 0.5
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ describe "#set, #cancel" do
114
+
115
+ context "when #set called after #cancel" do
116
+ it "should raise QuackConcurrency::Future::Complete" do
117
+ future = QuackConcurrency::Future.new
118
+ future.cancel
119
+ expect{ future.set(1) }.to raise_error(QuackConcurrency::Future::Complete)
120
+ end
121
+ end
122
+
123
+ context "when #cancel called after #set" do
124
+ it "should raise QuackConcurrency::Future::Complete" do
125
+ future = QuackConcurrency::Future.new
126
+ future.set(1)
127
+ expect{ future.cancel }.to raise_error(QuackConcurrency::Future::Complete)
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ describe "#get, #cancel" do
134
+
135
+ context "when #get called after #cancel" do
136
+ it "should raise QuackConcurrency::Future::Canceled" do
137
+ future = QuackConcurrency::Future.new
138
+ future.cancel
139
+ expect{ future.get }.to raise_error(QuackConcurrency::Future::Canceled)
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ end
data/spec/queue_spec.rb CHANGED
@@ -2,50 +2,151 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::Queue do
4
4
 
5
- describe "pop" do
5
+ describe "::new" do
6
6
 
7
- context "when called when queue empty" do
8
- it "should wait" do
9
- $test = []
7
+ context "when called without a 'duck_types' argument" do
8
+ it "should create a new QuackConcurrency::Queue" do
9
+ queue = QuackConcurrency::Queue.new
10
+ expect(queue).to be_a(QuackConcurrency::Queue)
11
+ end
12
+ end
13
+
14
+ context "when called with 'condition_variable' and 'mutex' duck types" do
15
+ it "should create a new QuackConcurrency::Queue" do
16
+ duck_types = {condition_variable: Class.new, mutex: Class.new}
17
+ queue = QuackConcurrency::Queue.new(duck_types: duck_types)
18
+ expect(queue).to be_a(QuackConcurrency::Queue)
19
+ end
20
+ end
21
+
22
+ context "when called with only 'condition_variable' duck type" do
23
+ it "should raise ArgumentError" do
24
+ duck_types = {condition_variable: Class.new}
25
+ expect{ QuackConcurrency::Queue.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+
29
+ context "when called with only 'mutex' duck type" do
30
+ it "should raise ArgumentError" do
31
+ duck_types = {mutex: Class.new}
32
+ expect{ QuackConcurrency::Queue.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ describe "#push" do
39
+
40
+ context "when called many times when queue is not closed" do
41
+ it "should not raise error" do
42
+ queue = QuackConcurrency::Queue.new
43
+ expect{ queue.push(1) }.not_to raise_error
44
+ expect{ queue.push(2) }.not_to raise_error
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ describe "#close, #push" do
51
+
52
+ context "when called when queue is closed" do
53
+ it "should raise ClosedQueueError" do
54
+ queue = QuackConcurrency::Queue.new
55
+ queue.close
56
+ expect{ queue.push(1) }.to raise_error(ClosedQueueError)
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ describe "#pop, #close" do
63
+
64
+ context "when #pop is called after #close on empty queue" do
65
+ it "should reutrn nil" do
66
+ queue = QuackConcurrency::Queue.new
67
+ queue.close
68
+ expect(queue.pop).to eql nil
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ describe "#pop, #close" do
75
+
76
+ context "when #pop is called before #close on empty queue" do
77
+ it "should wait for #close then reutrn nil" do
10
78
  queue = QuackConcurrency::Queue.new
11
79
  thread = Thread.new do
12
- queue.pop
13
- $test << 1
14
80
  sleep 1
15
- queue.push
81
+ queue.close
16
82
  end
17
- sleep 1
18
- queue.push
19
- queue.pop
20
- $test << 2
83
+ start_time = Time.now
84
+ expect(queue.pop).to eql nil
85
+ end_time = Time.now
86
+ duration = end_time - start_time
21
87
  thread.join
22
- expect($test).to eql [1, 2]
88
+ expect(duration).to be > 0.5
23
89
  end
24
90
  end
25
91
 
26
- context "when called when queue not empty" do
27
- it "should immediately return" do
92
+ end
93
+
94
+ describe "#pop, #push, #close" do
95
+
96
+ context "when #pop is called after #close on queue with one item" do
97
+ it "should reutrn item" do
98
+ queue = QuackConcurrency::Queue.new
99
+ queue.push(1)
100
+ queue.close
101
+ expect(queue.pop).to eql 1
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ describe "#pop, #push" do
108
+
109
+ context "when #pop is called before #push" do
110
+ it "should wait until #push is called" do
28
111
  $test = []
29
112
  queue = QuackConcurrency::Queue.new
30
- queue.push
31
113
  thread = Thread.new do
32
114
  queue.pop
33
115
  $test << 1
116
+ sleep 1
117
+ queue.push
34
118
  end
35
119
  sleep 1
120
+ queue.push
121
+ queue.pop
36
122
  $test << 2
37
123
  thread.join
38
124
  expect($test).to eql [1, 2]
39
125
  end
40
126
  end
41
-
42
- context "when called" do
43
- it "should return value of the push" do
127
+
128
+ end
129
+
130
+ describe "#pop, #push, #clear" do
131
+
132
+ context "when #pop is called after #push but before #clear" do
133
+ it "should wait until #push is called again" do
44
134
  queue = QuackConcurrency::Queue.new
45
- queue.push 1
46
- expect(queue.pop).to eql 1
135
+ queue.push(1)
136
+ queue.clear
137
+ thread = Thread.new do
138
+ sleep 1
139
+ queue.push(2)
140
+ end
141
+ start_time = Time.now
142
+ expect(queue.pop).to eql 2
143
+ end_time = Time.now
144
+ duration = end_time - start_time
145
+ thread.join
146
+ expect(duration).to be > 0.5
47
147
  end
48
148
  end
49
-
149
+
50
150
  end
151
+
51
152
  end
@@ -2,7 +2,40 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::ReentrantMutex do
4
4
 
5
- describe "lock" do
5
+ describe "::new" do
6
+
7
+ context "when called without a 'duck_types' argument" do
8
+ it "should create a new QuackConcurrency::ReentrantMutex" do
9
+ mutex = QuackConcurrency::ReentrantMutex.new
10
+ expect(mutex).to be_a(QuackConcurrency::ReentrantMutex)
11
+ end
12
+ end
13
+
14
+ context "when called with 'condition_variable' and 'mutex' duck types" do
15
+ it "should create a new QuackConcurrency::ReentrantMutex" do
16
+ duck_types = {condition_variable: Class.new, mutex: Class.new}
17
+ mutex = QuackConcurrency::ReentrantMutex.new(duck_types: duck_types)
18
+ expect(mutex).to be_a(QuackConcurrency::ReentrantMutex)
19
+ end
20
+ end
21
+
22
+ context "when called with only 'condition_variable' duck type" do
23
+ it "should raise ArgumentError" do
24
+ duck_types = {condition_variable: Class.new}
25
+ expect{ QuackConcurrency::ReentrantMutex.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+
29
+ context "when called with only 'mutex' duck type" do
30
+ it "should raise ArgumentError" do
31
+ duck_types = {mutex: Class.new}
32
+ expect{ QuackConcurrency::ReentrantMutex.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ describe "#lock" do
6
39
 
7
40
  context "when called for first time" do
8
41
  it "should not raise error" do
@@ -11,7 +44,7 @@ RSpec.describe QuackConcurrency::ReentrantMutex do
11
44
  end
12
45
  end
13
46
 
14
- context "when called the second time" do
47
+ context "when called a second time" do
15
48
  it "should not raise error" do
16
49
  mutex = QuackConcurrency::ReentrantMutex.new
17
50
  mutex.lock
@@ -19,29 +52,29 @@ RSpec.describe QuackConcurrency::ReentrantMutex do
19
52
  end
20
53
  end
21
54
 
22
- context "when called on non owning thread" do
23
- it "should wait" do
24
- $test = []
55
+ end
56
+
57
+ describe "#lock, #unlock" do
58
+
59
+ context "when #lock called on non owning thread" do
60
+ it "should wait for #unlock" do
25
61
  mutex = QuackConcurrency::ReentrantMutex.new
26
62
  thread = Thread.new do
27
- sleep 1
28
63
  mutex.lock
29
- $test << 2
64
+ sleep 2
65
+ mutex.unlock
30
66
  end
67
+ sleep 1
68
+ start_time = Time.now
31
69
  mutex.lock
32
- sleep 2
33
- $test << 1
34
- mutex.unlock
70
+ end_time = Time.now
71
+ duration = end_time - start_time
35
72
  thread.join
36
- expect($test).to eql [1, 2]
73
+ expect(duration).to be > 0.5
37
74
  end
38
75
  end
39
76
 
40
- end
41
-
42
- describe "unlock" do
43
-
44
- context "when called after one lock" do
77
+ context "when #unlock called after one #lock" do
45
78
  it "should not raise error" do
46
79
  mutex = QuackConcurrency::ReentrantMutex.new
47
80
  mutex.lock
@@ -49,7 +82,7 @@ RSpec.describe QuackConcurrency::ReentrantMutex do
49
82
  end
50
83
  end
51
84
 
52
- context "when called after two locks" do
85
+ context "when #unlock called after two #locks" do
53
86
  it "should not raise error" do
54
87
  mutex = QuackConcurrency::ReentrantMutex.new
55
88
  mutex.lock
@@ -58,14 +91,108 @@ RSpec.describe QuackConcurrency::ReentrantMutex do
58
91
  end
59
92
  end
60
93
 
61
- context "when called twice after only one lock" do
94
+ context "when #unlock called twice after only one #lock" do
62
95
  it "should raise error" do
63
96
  mutex = QuackConcurrency::ReentrantMutex.new
64
97
  mutex.lock
65
98
  mutex.unlock
66
- expect { mutex.unlock }.to raise_error(RuntimeError)
99
+ expect { mutex.unlock }.to raise_error(QuackConcurrency::ReentrantMutex::Error)
67
100
  end
68
101
  end
69
102
 
70
103
  end
104
+
105
+ describe "#lock, #try_lock" do
106
+
107
+ context "when #try_lock called" do
108
+ it "should reutrn true" do
109
+ mutex = QuackConcurrency::ReentrantMutex.new
110
+ expect(mutex.try_lock).to eql true
111
+ end
112
+ end
113
+
114
+ context "when #try_lock called after #lock called from other Thread" do
115
+ it "should reutrn false" do
116
+ mutex = QuackConcurrency::ReentrantMutex.new
117
+ thread = Thread.new { mutex.lock }
118
+ sleep 1
119
+ expect(mutex.try_lock).to eql false
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ describe "#lock, #try_lock, #unlock" do
126
+
127
+ context "when #lock called after #try_lock called from other Thread" do
128
+ it "should wait for #unlock" do
129
+ mutex = QuackConcurrency::ReentrantMutex.new
130
+ thread = Thread.new do
131
+ mutex.try_lock
132
+ sleep 2
133
+ mutex.unlock
134
+ end
135
+ sleep 1
136
+ start_time = Time.now
137
+ mutex.lock
138
+ end_time = Time.now
139
+ duration = end_time - start_time
140
+ thread.join
141
+ expect(duration).to be > 0.5
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ describe "#synchronize" do
148
+
149
+ context "when #synchronize called" do
150
+ it "should return last value from block" do
151
+ mutex = QuackConcurrency::ReentrantMutex.new
152
+ value = mutex.synchronize do
153
+ 1
154
+ end
155
+ expect(value).to eql 1
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ describe "#sleep" do
162
+
163
+ context "when #sleep called with time argument" do
164
+ it "should wait for that time" do
165
+ mutex = QuackConcurrency::ReentrantMutex.new
166
+ start_time = Time.now
167
+ mutex.synchronize do
168
+ mutex.sleep(1)
169
+ end
170
+ end_time = Time.now
171
+ duration = end_time - start_time
172
+ expect(duration).to be > 0.5
173
+ end
174
+ end
175
+
176
+ context "when #sleep called with no time argument" do
177
+ it "should wait until Thread is resumed" do
178
+ mutex = QuackConcurrency::ReentrantMutex.new
179
+ start_time = nil
180
+ end_time = nil
181
+ thread = Thread.new do
182
+ start_time = Time.now
183
+ mutex.synchronize do
184
+ mutex.sleep
185
+ end
186
+ end_time = Time.now
187
+ end
188
+ sleep 1
189
+ thread.run
190
+ thread.join
191
+ duration = end_time - start_time
192
+ expect(duration).to be > 0.5
193
+ end
194
+ end
195
+
196
+ end
197
+
71
198
  end
@@ -2,26 +2,276 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::Semaphore do
4
4
 
5
- describe "acquire" do
5
+ describe "::new" do
6
6
 
7
- context "when no permits are available" do
8
- it "should wait" do
9
- $test = []
7
+ context "when called without a 'duck_types' argument" do
8
+ it "should create a new QuackConcurrency::Semaphore" do
9
+ semaphore = QuackConcurrency::Semaphore.new
10
+ expect(semaphore).to be_a(QuackConcurrency::Semaphore)
11
+ end
12
+ end
13
+
14
+ context "when called with 'condition_variable' and 'mutex' duck types" do
15
+ it "should create a new QuackConcurrency::Semaphore" do
16
+ duck_types = {condition_variable: Class.new, mutex: Class.new}
17
+ semaphore = QuackConcurrency::Semaphore.new(duck_types: duck_types)
18
+ expect(semaphore).to be_a(QuackConcurrency::Semaphore)
19
+ end
20
+ end
21
+
22
+ context "when called with only 'condition_variable' duck type" do
23
+ it "should raise ArgumentError" do
24
+ duck_types = {condition_variable: Class.new}
25
+ expect{ QuackConcurrency::Semaphore.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+
29
+ context "when called with only 'mutex' duck type" do
30
+ it "should raise ArgumentError" do
31
+ duck_types = {mutex: Class.new}
32
+ expect{ QuackConcurrency::Semaphore.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ describe "#release" do
39
+
40
+ context "when called for the first time with many permits available" do
41
+ it "should not raise error" do
42
+ semaphore = QuackConcurrency::Semaphore.new(2)
43
+
44
+ expect{ semaphore.release }.not_to raise_error
45
+ end
46
+ end
47
+
48
+ context "when called a second time with one permit available" do
49
+ it "should not raise error" do
10
50
  semaphore = QuackConcurrency::Semaphore.new(2)
51
+ semaphore.release
52
+ expect{ semaphore.release }.not_to raise_error
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ describe "#release, #reacquire" do
59
+
60
+ context "when #release called with no permits available" do
61
+ it "should wait until #reacquire is called" do
62
+ semaphore = QuackConcurrency::Semaphore.new(2)
63
+ semaphore.release
64
+ semaphore.release
11
65
  thread = Thread.new do
12
66
  sleep 1
13
- semaphore.acquire
14
- $test << 2
67
+ semaphore.reacquire
15
68
  end
16
- semaphore.acquire
17
- semaphore.acquire
18
- sleep 2
19
- $test << 1
69
+ start_time = Time.now
20
70
  semaphore.release
71
+ end_time = Time.now
72
+ duration = end_time - start_time
21
73
  thread.join
22
- expect($test).to eql [1, 2]
74
+ expect(duration).to be > 0.5
75
+ end
76
+ end
77
+
78
+ context "when #reacquire called when all permits are available" do
79
+ it "should raise QuackConcurrency::Semaphore::Error" do
80
+ semaphore = QuackConcurrency::Semaphore.new(2)
81
+ expect{ semaphore.reacquire }.to raise_error(QuackConcurrency::Semaphore::Error)
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ describe "#release, #reacquire, #permit_available?, #permits_available" do
88
+
89
+ context "#permit_available? and #permits_available" do
90
+ it "should work as expected" do
91
+ semaphore = QuackConcurrency::Semaphore.new(2)
92
+ expect(semaphore.permit_available?).to eql true
93
+ expect(semaphore.permits_available).to eql 2
94
+ semaphore.release
95
+ expect(semaphore.permit_available?).to eql true
96
+ expect(semaphore.permits_available).to eql 1
97
+ semaphore.release
98
+ expect(semaphore.permit_available?).to eql false
99
+ expect(semaphore.permits_available).to eql 0
100
+ semaphore.reacquire
101
+ expect(semaphore.permit_available?).to eql true
102
+ expect(semaphore.permits_available).to eql 1
103
+ semaphore.reacquire
104
+ expect(semaphore.permit_available?).to eql true
105
+ expect(semaphore.permits_available).to eql 2
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ describe "#set_permit_count, #permits_available" do
112
+
113
+ context "when #set_permit_count is called with more permits then currently exist" do
114
+ it "should add permits" do
115
+ semaphore = QuackConcurrency::Semaphore.new(2)
116
+ expect(semaphore.permits_available).to eql 2
117
+ semaphore.set_permit_count(3)
118
+ expect(semaphore.permits_available).to eql 3
119
+ end
120
+ end
121
+
122
+ context "when #set_permit_count is called with less permits then currently exist" do
123
+ it "should remove permits" do
124
+ semaphore = QuackConcurrency::Semaphore.new(2)
125
+ expect(semaphore.permits_available).to eql 2
126
+ semaphore.set_permit_count(1)
127
+ expect(semaphore.permits_available).to eql 1
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ describe "#set_permit_count, #permits_available, #release" do
134
+
135
+ context "when #set_permit_count is called with less permits then currently available" do
136
+ it "should raise QuackConcurrency::Semaphore::Error" do
137
+ semaphore = QuackConcurrency::Semaphore.new(2)
138
+ expect(semaphore.permits_available).to eql 2
139
+ semaphore.release
140
+ semaphore.release
141
+ expect{ semaphore.set_permit_count(1) }.to raise_error(QuackConcurrency::Semaphore::Error)
23
142
  end
24
143
  end
25
144
 
26
145
  end
146
+
147
+ describe "#set_permit_count, #release" do
148
+
149
+ context "when #set_permit_count is called with more permits when one thread is waiting on #release" do
150
+ it "should resume the thread" do
151
+ semaphore = QuackConcurrency::Semaphore.new(2)
152
+ semaphore.release
153
+ semaphore.release
154
+ thread = Thread.new do
155
+ sleep 1
156
+ semaphore.set_permit_count(3)
157
+ end
158
+ start_time = Time.now
159
+ semaphore.release
160
+ end_time = Time.now
161
+ duration = end_time - start_time
162
+ thread.join
163
+ expect(duration).to be > 0.5
164
+ end
165
+ end
166
+
167
+ end
168
+
169
+ describe "#set_permit_count!, #permits_available" do
170
+
171
+ context "when #set_permit_count! is called with more permits then currently exist" do
172
+ it "should add permits" do
173
+ semaphore = QuackConcurrency::Semaphore.new(2)
174
+ expect(semaphore.permits_available).to eql 2
175
+ semaphore.set_permit_count!(3)
176
+ expect(semaphore.permits_available).to eql 3
177
+ end
178
+ end
179
+
180
+ context "when #set_permit_count! is called with less permits then currently exist" do
181
+ it "should remove permits" do
182
+ semaphore = QuackConcurrency::Semaphore.new(2)
183
+ expect(semaphore.permits_available).to eql 2
184
+ semaphore.set_permit_count!(1)
185
+ expect(semaphore.permits_available).to eql 1
186
+ end
187
+ end
188
+
189
+ end
190
+
191
+ describe "#set_permit_count!, #permits_available, #release, #reacquire" do
192
+
193
+ context "when #set_permit_count! is called with less permits then currently available" do
194
+ it "should force new permit count" do
195
+ semaphore = QuackConcurrency::Semaphore.new(2)
196
+ expect(semaphore.permits_available).to eql 2
197
+ semaphore.release
198
+ semaphore.release
199
+ semaphore.set_permit_count!(1)
200
+ expect(semaphore.permit_available?).to eql false
201
+ semaphore.reacquire
202
+ expect(semaphore.permit_available?).to eql false
203
+ semaphore.reacquire
204
+ expect(semaphore.permit_available?).to eql true
205
+ end
206
+ end
207
+
208
+ end
209
+
210
+ describe "#set_permit_count!, #release" do
211
+
212
+ context "when #set_permit_count! is called with more permits when one thread is waiting on #release" do
213
+ it "should resume the thread" do
214
+ semaphore = QuackConcurrency::Semaphore.new(2)
215
+ semaphore.release
216
+ semaphore.release
217
+ thread = Thread.new do
218
+ sleep 1
219
+ semaphore.set_permit_count(3)
220
+ end
221
+ start_time = Time.now
222
+ semaphore.release
223
+ end_time = Time.now
224
+ duration = end_time - start_time
225
+ thread.join
226
+ expect(duration).to be > 0.5
227
+ end
228
+ end
229
+
230
+ end
231
+
232
+ describe "#set_permit_count!, #release, #permit_available?" do
233
+
234
+ context "when semaphore has no permits available, them #set_permit_count! is called to remove 2 permits, then called again to add 1 permit" do
235
+ it "should not have any permits available" do
236
+ semaphore = QuackConcurrency::Semaphore.new(3)
237
+ semaphore.release
238
+ semaphore.release
239
+ expect(semaphore.permit_available?).to eql true
240
+ semaphore.set_permit_count!(1)
241
+ expect(semaphore.permit_available?).to eql false
242
+ semaphore.set_permit_count!(2)
243
+ expect(semaphore.permit_available?).to eql false
244
+ end
245
+ end
246
+
247
+ end
248
+
249
+ describe "#set_permit_count!, #release, #reacquire" do
250
+
251
+ context "when semaphore has no permits available, them #set_permit_count! is called to remove 2 permits, then a thread starts waiting for #release, then #set_permit_count! is called again to add 1 permit" do
252
+ it "thread should wait for #reacquire to be called" do
253
+ semaphore = QuackConcurrency::Semaphore.new(3)
254
+ semaphore.release
255
+ semaphore.release
256
+ semaphore.release
257
+ semaphore.set_permit_count!(1)
258
+ thread = Thread.new do
259
+ sleep 1
260
+ semaphore.set_permit_count!(2)
261
+ sleep 1
262
+ semaphore.reacquire
263
+ sleep 1
264
+ semaphore.reacquire
265
+ end
266
+ start_time = Time.now
267
+ semaphore.release
268
+ end_time = Time.now
269
+ duration = end_time - start_time
270
+ thread.join
271
+ expect(duration).to be_between(2.5, 3.5)
272
+ end
273
+ end
274
+
275
+ end
276
+
27
277
  end
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quack_concurrency
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Fors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-14 00:00:00.000000000 Z
11
+ date: 2018-04-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Offers concurrency tools that could also be found in the Concurrent Ruby
14
- project. However, all these tools will also accept duck types to allow core classes
15
- to behave as desired.
13
+ description: Offers concurrency tools that could also be found in the 'Concurrent
14
+ Ruby'. However, all these tools will also accept core class ducktypes to build off
15
+ of.
16
16
  email: mail@robfors.com
17
17
  executables: []
18
18
  extensions: []
@@ -21,11 +21,19 @@ files:
21
21
  - LICENSE
22
22
  - README.md
23
23
  - lib/quack_concurrency.rb
24
+ - lib/quack_concurrency/concurrency_tool.rb
25
+ - lib/quack_concurrency/error.rb
24
26
  - lib/quack_concurrency/future.rb
27
+ - lib/quack_concurrency/future/canceled.rb
28
+ - lib/quack_concurrency/future/complete.rb
29
+ - lib/quack_concurrency/name.rb
25
30
  - lib/quack_concurrency/queue.rb
26
31
  - lib/quack_concurrency/reentrant_mutex.rb
32
+ - lib/quack_concurrency/reentrant_mutex/error.rb
27
33
  - lib/quack_concurrency/semaphore.rb
34
+ - lib/quack_concurrency/semaphore/error.rb
28
35
  - lib/quack_concurrency/waiter.rb
36
+ - spec/future_spec.rb
29
37
  - spec/queue_spec.rb
30
38
  - spec/reentrant_mutex_spec.rb
31
39
  - spec/semaphore_spec.rb
@@ -50,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
58
  version: '0'
51
59
  requirements: []
52
60
  rubyforge_project:
53
- rubygems_version: 2.4.8
61
+ rubygems_version: 2.7.6
54
62
  signing_key:
55
63
  specification_version: 4
56
64
  summary: Concurrency tools that accept duck types of core classes.