dunder 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/Gemfile +4 -8
- data/Gemfile.lock +17 -1
- data/Readme.md +98 -35
- data/VERSION +1 -1
- data/dunder.gemspec +16 -10
- data/lib/dunder.rb +129 -28
- data/test/helper.rb +41 -1
- data/test/test.sqlite3 +0 -0
- data/test/test_dunder.rb +121 -24
- data/test/test_dunder_group.rb +101 -0
- metadata +34 -45
data/.gemtest
ADDED
File without changes
|
data/Gemfile
CHANGED
@@ -1,15 +1,11 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
|
-
# Add dependencies required to use your gem here.
|
3
|
-
# Example:
|
4
|
-
gem "activesupport", ">= 3.0.3"
|
5
|
-
gem "activerecord", ">= 3.0.3"
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
# Include everything needed to run rake, tests, features, etc.
|
10
|
-
group :development do
|
3
|
+
group :development, :test do
|
4
|
+
gem "activerecord", ">= 3.0.3"
|
11
5
|
gem "shoulda", ">= 0"
|
12
6
|
gem "bundler", "~> 1.0.0"
|
13
7
|
gem "jeweler", "~> 1.5.2"
|
14
8
|
gem "rcov", ">= 0"
|
9
|
+
gem 'sqlite3'
|
10
|
+
gem (RUBY_VERSION > "1.9" ? 'ruby-debug19' : 'ruby-debug')
|
15
11
|
end
|
data/Gemfile.lock
CHANGED
@@ -11,17 +11,32 @@ GEM
|
|
11
11
|
arel (~> 2.0.2)
|
12
12
|
tzinfo (~> 0.3.23)
|
13
13
|
activesupport (3.0.3)
|
14
|
+
archive-tar-minitar (0.5.2)
|
14
15
|
arel (2.0.7)
|
15
16
|
builder (2.1.2)
|
17
|
+
columnize (0.3.2)
|
16
18
|
git (1.2.5)
|
17
19
|
i18n (0.5.0)
|
18
20
|
jeweler (1.5.2)
|
19
21
|
bundler (~> 1.0.0)
|
20
22
|
git (>= 1.2.5)
|
21
23
|
rake
|
24
|
+
linecache19 (0.5.11)
|
25
|
+
ruby_core_source (>= 0.1.4)
|
22
26
|
rake (0.8.7)
|
23
27
|
rcov (0.9.9)
|
28
|
+
ruby-debug-base19 (0.11.24)
|
29
|
+
columnize (>= 0.3.1)
|
30
|
+
linecache19 (>= 0.5.11)
|
31
|
+
ruby_core_source (>= 0.1.4)
|
32
|
+
ruby-debug19 (0.11.6)
|
33
|
+
columnize (>= 0.3.1)
|
34
|
+
linecache19 (>= 0.5.11)
|
35
|
+
ruby-debug-base19 (>= 0.11.19)
|
36
|
+
ruby_core_source (0.1.4)
|
37
|
+
archive-tar-minitar (>= 0.5.2)
|
24
38
|
shoulda (2.11.3)
|
39
|
+
sqlite3 (1.3.3)
|
25
40
|
tzinfo (0.3.24)
|
26
41
|
|
27
42
|
PLATFORMS
|
@@ -29,8 +44,9 @@ PLATFORMS
|
|
29
44
|
|
30
45
|
DEPENDENCIES
|
31
46
|
activerecord (>= 3.0.3)
|
32
|
-
activesupport (>= 3.0.3)
|
33
47
|
bundler (~> 1.0.0)
|
34
48
|
jeweler (~> 1.5.2)
|
35
49
|
rcov
|
50
|
+
ruby-debug19
|
36
51
|
shoulda
|
52
|
+
sqlite3
|
data/Readme.md
CHANGED
@@ -1,50 +1,66 @@
|
|
1
|
+
Dunder
|
2
|
+
=========================
|
3
|
+
For tasks that can be started _early_ and evaluated _late_.
|
4
|
+
|
1
5
|
A simple way of doing heavy work in a background process and blocking until done when you really need the object.
|
2
6
|
|
3
7
|
Preloading using the [proxy pattern](http://sourcemaking.com/design_patterns/proxy)
|
4
8
|
Heavily inspired by Adam Sandersons [post](http://endofline.wordpress.com/2011/01/18/ruby-standard-library-delegator/)
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
10
|
+
#### Introduction
|
11
|
+
To increase performance typically one might want start multiple heavy tasks concurrent.
|
12
|
+
This is already solvable with threads or the [reactor-pattern](http://rubyeventmachine.com/) but setting this up could be cumbersome or require direct interactions with threads etc.
|
9
13
|
|
10
|
-
|
11
|
-
|
14
|
+
What inspired me was the ability to run concurrent database queries within a single request in rails, please read more in the section below.
|
15
|
+
|
16
|
+
How you could lazy load something today in ruby 1.9
|
17
|
+
|
18
|
+
foo = "foo"
|
19
|
+
bar = "bar"
|
20
|
+
t = Thread.start {
|
21
|
+
sleep 1
|
22
|
+
foo + bar
|
23
|
+
}
|
24
|
+
# Other code
|
25
|
+
foobar = t.value
|
26
|
+
|
27
|
+
The Thread.start call would not block and execution would continue and when you need the value you could ask t.value for it
|
12
28
|
|
13
|
-
Dunder is a simple way of abstracting this:
|
14
|
-
|
15
|
-
|
29
|
+
Dunder is a simple way of abstracting this but does infact use threads behind the scenes: you simply pass a block to Dunder.lazy_load
|
30
|
+
When later accessing the returned object,
|
31
|
+
lets say: lazy_object will block until the thread is done and has returned or if the thread is done returns the value.
|
16
32
|
|
17
|
-
|
33
|
+
Dunder will only be happy under 1.9.* because how blocks changed have changed. There also some caveats that you _should_ read about below
|
18
34
|
|
19
|
-
Usage
|
35
|
+
#### Usage
|
20
36
|
|
21
|
-
lazy_object = Dunder.
|
37
|
+
lazy_object = Dunder.lazy_load {
|
22
38
|
# heavy stuff
|
23
39
|
value
|
24
40
|
}
|
25
|
-
|
26
|
-
|
41
|
+
# Later on access lazy_object
|
42
|
+
puts lazy_object
|
43
|
+
puts lazy_object.class # => value.class
|
27
44
|
|
28
|
-
|
29
|
-
a.title
|
30
|
-
end
|
45
|
+
or through chaining with dunder_load which works for both objects and classes
|
31
46
|
|
32
47
|
lazy_sorted_array = array.dunder_load.sort
|
33
48
|
|
49
|
+
# With arguments and block
|
34
50
|
lazy_obj = obj.dunder_load.do_something_heavy(a,b,c) {
|
35
51
|
#maybe something other heavy here
|
36
52
|
}
|
37
53
|
|
38
|
-
|
54
|
+
Parallel example
|
39
55
|
|
40
|
-
lazy_foo = Dunder.
|
41
|
-
# Simulate heavy
|
56
|
+
lazy_foo = Dunder.lazy_load {
|
57
|
+
# Simulate heavy work
|
42
58
|
sleep 2
|
43
59
|
"foo"
|
44
60
|
}
|
45
61
|
|
46
|
-
lazy_bar = Dunder.
|
47
|
-
# Simulate heavy
|
62
|
+
lazy_bar = Dunder.lazy_load {
|
63
|
+
# Simulate heavy work
|
48
64
|
sleep 2
|
49
65
|
"bar"
|
50
66
|
}
|
@@ -59,7 +75,8 @@ Read more further down
|
|
59
75
|
|
60
76
|
worth mentioning is that if you access the variable in someway before that it will block earlier
|
61
77
|
ex
|
62
|
-
|
78
|
+
|
79
|
+
lazy_array = Dunder.lazy_load do
|
63
80
|
sleep 1
|
64
81
|
[1,2,3]
|
65
82
|
end
|
@@ -69,7 +86,7 @@ ex
|
|
69
86
|
|
70
87
|
changing the order of the statements will fix this though
|
71
88
|
|
72
|
-
lazy_array = Dunder.
|
89
|
+
lazy_array = Dunder.lazy_load do
|
73
90
|
sleep 1
|
74
91
|
[1,2,3]
|
75
92
|
end
|
@@ -77,27 +94,73 @@ changing the order of the statements will fix this though
|
|
77
94
|
puts lazy_array.length # <- will block here until the above sleep in the block is done
|
78
95
|
puts lazy_array # <- will be printed after 1 second
|
79
96
|
|
80
|
-
|
81
|
-
Rails
|
97
|
+
WARNING "if-it-quacks-like-a-duck-walks-like-a-duck"
|
82
98
|
====================
|
99
|
+
* Don't return symbols
|
100
|
+
* And for normal objects be careful with comparing
|
101
|
+
|
102
|
+
The reason for this is that the implementation uses the delegation.rb in ruby which makes objects life tricky. Even though the object return quacks like a object it will not always walk in a straight line.
|
103
|
+
Ex
|
104
|
+
|
105
|
+
o = Object.new
|
106
|
+
res = Dunder.lazy_load { o }
|
107
|
+
res == o # => true
|
108
|
+
o == res # => false
|
109
|
+
o == res._thread.value # => true
|
110
|
+
|
111
|
+
But Array,String,Fixnum,Hash etc work fine.
|
83
112
|
|
84
|
-
|
85
|
-
|
113
|
+
If you want to be sure that nothing fishy is going on please use ._thread.value
|
114
|
+
|
115
|
+
Groups
|
116
|
+
====================
|
117
|
+
So now you might be wondering what would happen if we lazy load more than 10000 objects through some intense calculations. Well our performance would decrease because of the [context switching](http://en.wikipedia.org/wiki/Context_switch), it would actually be better if we only ran a limited number of lazy loads at any one point.
|
118
|
+
|
119
|
+
Dunder::Group has been specifically designed to solve this problem.
|
120
|
+
|
121
|
+
For our contrived example note that this example and dunder requires a ruby version of at least 1.9.* . Lets say we have list of tens of thousands of urls that we want to visit and measure the sum content length of all the websites
|
122
|
+
|
123
|
+
require 'open-uri'
|
124
|
+
list = ["http://google.com","http://yahoo.com", .... ]
|
125
|
+
g = Dunder::Group.new(100)
|
126
|
+
results = list.map do |u|
|
127
|
+
g.lazy_load { open(u) }
|
128
|
+
# or dunder_load(g).open(u)
|
86
129
|
end
|
87
|
-
|
88
|
-
|
130
|
+
|
131
|
+
sum = 0
|
132
|
+
results.each do |r|
|
133
|
+
sum += r.length
|
89
134
|
end
|
135
|
+
puts sum
|
136
|
+
|
137
|
+
Note that you could use groups by itself and pass blocks
|
138
|
+
|
139
|
+
g = Dunder::Group.new(4)
|
140
|
+
t = g.start_thread {
|
141
|
+
# things to do here
|
142
|
+
}
|
143
|
+
|
144
|
+
Much depending on what you are doing you will want to pick a higher or lower number. If your task is CPU-bound then around the number of cores on your computer should be optimal, if your task is IO bound which is true for most of my use cases then experimenting is key.
|
145
|
+
|
146
|
+
Rails
|
147
|
+
====================
|
148
|
+
|
149
|
+
# Will not block
|
150
|
+
@lazy_posts = Post.dunder_load.all
|
151
|
+
|
152
|
+
@lazy_user = User.dunder_load.first
|
90
153
|
|
91
154
|
and then later in views
|
92
155
|
|
93
|
-
<%= @
|
156
|
+
<%= @user.name %> <- will block until the user have been loaded
|
157
|
+
<%= @lazyposts.each do %> <- will block until the posts have been loaded
|
94
158
|
...
|
95
|
-
<% end
|
96
|
-
|
97
|
-
|
98
|
-
Known problems
|
159
|
+
<% end %
|
160
|
+
Be careful not to use the mysql gem which blocks the whole universe on every call. Please use the mysql2 which is the standard adapter for rails since 3.0,
|
161
|
+
also the pg gem works fine.
|
99
162
|
|
100
|
-
|
163
|
+
For a sample application using mysql checkout [this](https://github.com/Fonsan/dunder-rails-demo)
|
101
164
|
|
102
165
|
Install
|
103
166
|
=======
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/dunder.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{dunder}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Fonsan"]
|
12
|
-
s.date = %q{2011-
|
12
|
+
s.date = %q{2011-02-16}
|
13
13
|
s.description = %q{For tasks that can be started early and evaluated late.
|
14
14
|
|
15
15
|
Typically one might want start multiple heavy tasks concurrent.
|
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
"LICENSE.txt"
|
23
23
|
]
|
24
24
|
s.files = [
|
25
|
+
".gemtest",
|
25
26
|
"Gemfile",
|
26
27
|
"Gemfile.lock",
|
27
28
|
"LICENSE.txt",
|
@@ -31,44 +32,49 @@ Gem::Specification.new do |s|
|
|
31
32
|
"dunder.gemspec",
|
32
33
|
"lib/dunder.rb",
|
33
34
|
"test/helper.rb",
|
34
|
-
"test/
|
35
|
+
"test/test.sqlite3",
|
36
|
+
"test/test_dunder.rb",
|
37
|
+
"test/test_dunder_group.rb"
|
35
38
|
]
|
36
39
|
s.homepage = %q{http://github.com/Fonsan/dunder}
|
37
40
|
s.licenses = ["MIT"]
|
38
41
|
s.require_paths = ["lib"]
|
39
|
-
s.rubygems_version = %q{1.
|
42
|
+
s.rubygems_version = %q{1.5.2}
|
40
43
|
s.summary = %q{A simple way of doing heavy work in a background process and when you really need the object it will block until it is done}
|
41
44
|
s.test_files = [
|
42
45
|
"test/helper.rb",
|
43
|
-
"test/test_dunder.rb"
|
46
|
+
"test/test_dunder.rb",
|
47
|
+
"test/test_dunder_group.rb"
|
44
48
|
]
|
45
49
|
|
46
50
|
if s.respond_to? :specification_version then
|
47
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
51
|
s.specification_version = 3
|
49
52
|
|
50
53
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
51
|
-
s.
|
52
|
-
s.add_runtime_dependency(%q<activerecord>, [">= 3.0.3"])
|
54
|
+
s.add_development_dependency(%q<activerecord>, [">= 3.0.3"])
|
53
55
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
54
56
|
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
55
57
|
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
56
58
|
s.add_development_dependency(%q<rcov>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<sqlite3>, [">= 0"])
|
60
|
+
s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
|
57
61
|
else
|
58
|
-
s.add_dependency(%q<activesupport>, [">= 3.0.3"])
|
59
62
|
s.add_dependency(%q<activerecord>, [">= 3.0.3"])
|
60
63
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
61
64
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
62
65
|
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
63
66
|
s.add_dependency(%q<rcov>, [">= 0"])
|
67
|
+
s.add_dependency(%q<sqlite3>, [">= 0"])
|
68
|
+
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
64
69
|
end
|
65
70
|
else
|
66
|
-
s.add_dependency(%q<activesupport>, [">= 3.0.3"])
|
67
71
|
s.add_dependency(%q<activerecord>, [">= 3.0.3"])
|
68
72
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
69
73
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
70
74
|
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
71
75
|
s.add_dependency(%q<rcov>, [">= 0"])
|
76
|
+
s.add_dependency(%q<sqlite3>, [">= 0"])
|
77
|
+
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
72
78
|
end
|
73
79
|
end
|
74
80
|
|
data/lib/dunder.rb
CHANGED
@@ -1,12 +1,42 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'thread'
|
2
3
|
class Dunder
|
4
|
+
|
3
5
|
class Future < SimpleDelegator
|
4
|
-
|
5
|
-
|
6
|
+
@@threads = {}
|
7
|
+
FORBIDDEN = [Symbol]
|
8
|
+
|
9
|
+
def self.threads
|
10
|
+
@@threads
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.ensure_threads_finished(timeout = nil)
|
14
|
+
@@threads.values.each do |t|
|
15
|
+
raise 'Thread did not timeout in time' unless t.join(timeout)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :_thread
|
20
|
+
|
21
|
+
def initialize(group = nil,&block)
|
22
|
+
raise ArgumentError,"No block was passed for execution" unless block
|
23
|
+
@_thread = group ? group.start_thread(&block) : Thread.start(&block)
|
24
|
+
@@threads[@_thread.object_id] = @_thread
|
6
25
|
end
|
7
26
|
|
8
27
|
def __getobj__
|
9
|
-
|
28
|
+
# Optimizing a bit
|
29
|
+
return super if @delegate_sd_obj
|
30
|
+
__setobj__(@_thread.value)
|
31
|
+
#@delegate_sd_obj = @_thread.value
|
32
|
+
if FORBIDDEN.include?(super.class)
|
33
|
+
error = "Your block returned a #{super.class} and because of how ruby handles #{FORBIDDEN.join(", ")}"
|
34
|
+
error << " the #{super.class} won't behave correctly. There are two known workarounds:"
|
35
|
+
error << " add the suffix ._thread.value or construct the block to return a array of length 1 and say lazy_array.first."
|
36
|
+
error << "Ex: puts lazy_object becomes lazy_object._thread.value"
|
37
|
+
raise ArgumentError,error
|
38
|
+
end
|
39
|
+
@@threads.delete @_thread.object_id
|
10
40
|
super
|
11
41
|
end
|
12
42
|
|
@@ -15,36 +45,107 @@ class Dunder
|
|
15
45
|
end
|
16
46
|
end
|
17
47
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
48
|
+
class Group
|
49
|
+
attr_reader :name,:max
|
50
|
+
|
51
|
+
def initialize(max)
|
52
|
+
raise ArgumentError,"You must specify a maximum number of threads for this group #{max}" unless max && max.is_a?(Integer)
|
53
|
+
@max = max
|
54
|
+
@running = 0
|
55
|
+
@waiting = []
|
56
|
+
@mutex = Mutex.new
|
25
57
|
end
|
26
58
|
|
27
|
-
def
|
28
|
-
@
|
59
|
+
def running
|
60
|
+
@mutex.synchronize {
|
61
|
+
@running
|
62
|
+
}
|
29
63
|
end
|
30
64
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
65
|
+
def waiting
|
66
|
+
@mutex.synchronize {
|
67
|
+
@waiting
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def lazy_load(&block)
|
72
|
+
Future.new(self,&block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def start_thread(&block)
|
76
|
+
group = self
|
77
|
+
Thread.start {
|
78
|
+
group.init_thread
|
79
|
+
value = block.call
|
80
|
+
group.finish_thread
|
81
|
+
value
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def init_thread
|
86
|
+
thread = Thread.current
|
87
|
+
waiting = false
|
88
|
+
@mutex.synchronize {
|
89
|
+
if waiting = (@running >= @max)
|
90
|
+
@waiting.push(thread)
|
91
|
+
else
|
92
|
+
@running += 1
|
93
|
+
end
|
94
|
+
}
|
95
|
+
Thread.stop if waiting
|
96
|
+
end
|
97
|
+
|
98
|
+
def finish_thread
|
99
|
+
thread = Thread.current
|
100
|
+
@mutex.synchronize {
|
101
|
+
@running -= 1
|
102
|
+
unless @waiting.empty?
|
103
|
+
# Schedule the next job
|
104
|
+
t = @waiting.shift
|
105
|
+
@running += 1
|
106
|
+
t.wakeup
|
107
|
+
end
|
108
|
+
}
|
35
109
|
end
|
36
110
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
Dunder::Dispacter.new(self)
|
111
|
+
|
112
|
+
# Kernel add exit hook to ensure all threads finishing before exiting
|
113
|
+
at_exit do
|
114
|
+
Future.ensure_threads_finished
|
42
115
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
116
|
+
|
117
|
+
module DunderMethod
|
118
|
+
def self.lazy_load(&block)
|
119
|
+
Future.new(&block)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.lazy_load(&block)
|
124
|
+
DunderMethod.lazy_load(&block)
|
125
|
+
end
|
126
|
+
|
127
|
+
# There maybe a better way of doing this
|
128
|
+
class Dispacter < (RUBY_VERSION > "1.9" ? BasicObject : Object)
|
129
|
+
def initialize(object,group = nil)
|
130
|
+
@_dunder_group = group
|
131
|
+
@_dunder_obj = object
|
132
|
+
end
|
50
133
|
|
134
|
+
def method_missing(method_sym, *arguments,&block)
|
135
|
+
disp = @_dunder_group ? @_dunder_group : DunderMethod
|
136
|
+
disp.lazy_load do
|
137
|
+
@_dunder_obj.send(method_sym, *arguments,&block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# http://olabini.com/blog/2011/01/safeer-monkey-patching/
|
143
|
+
# This also works for Class methods since: Class.ancestors => [Class, Module, Object, Kernel, BasicObject]
|
144
|
+
module Instance
|
145
|
+
def dunder_load(group = nil)
|
146
|
+
Dispacter.new(self,group)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
Object.send :include,Instance
|
150
|
+
|
151
|
+
end
|
data/test/helper.rb
CHANGED
@@ -7,12 +7,52 @@ rescue Bundler::BundlerError => e
|
|
7
7
|
$stderr.puts "Run `bundle install` to install missing gems"
|
8
8
|
exit e.status_code
|
9
9
|
end
|
10
|
+
require 'ruby-debug'
|
10
11
|
require 'test/unit'
|
11
12
|
require 'shoulda'
|
12
|
-
|
13
|
+
require 'timeout'
|
14
|
+
require 'active_record'
|
13
15
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
16
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
17
|
require 'dunder'
|
16
18
|
|
19
|
+
|
20
|
+
DBFILE ="test/test.sqlite3"
|
21
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => DBFILE)
|
22
|
+
|
23
|
+
unless File.exists? DBFILE
|
24
|
+
silence_stream(STDOUT) do
|
25
|
+
ActiveRecord::Schema.define do
|
26
|
+
create_table "posts", :force => true do |t|
|
27
|
+
t.string "name"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
class Post < ActiveRecord::Base; end
|
32
|
+
|
33
|
+
Post.create!(:name => "hello")
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class Post < ActiveRecord::Base; end
|
38
|
+
|
39
|
+
class Moods
|
40
|
+
def sleepy
|
41
|
+
"bar"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
17
45
|
class Test::Unit::TestCase
|
46
|
+
def setup
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def teardown
|
51
|
+
Dunder::Future.threads.values.each do |t|
|
52
|
+
timeout = 0.5
|
53
|
+
unless t.join(timeout)
|
54
|
+
raise "#{t} did not finish in #{timeout} seconds"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
18
58
|
end
|
data/test/test.sqlite3
ADDED
Binary file
|
data/test/test_dunder.rb
CHANGED
@@ -1,38 +1,135 @@
|
|
1
1
|
require 'helper'
|
2
|
-
require 'timeout'
|
3
2
|
class TestDunder < Test::Unit::TestCase
|
3
|
+
|
4
4
|
should "have some simple testing" do
|
5
5
|
b = "bar"
|
6
|
-
lazy_b =
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
lazy_b = Dunder.load {
|
11
|
-
sleep 1
|
12
|
-
"bar"
|
13
|
-
}
|
14
|
-
end
|
15
|
-
end
|
6
|
+
lazy_b = Dunder.lazy_load {
|
7
|
+
"bar"
|
8
|
+
}
|
16
9
|
assert_equal b,lazy_b
|
17
10
|
assert_equal b.class, lazy_b.class
|
18
11
|
end
|
19
12
|
|
13
|
+
should "return a equal object" do
|
14
|
+
objects = [Object.new,2,Class,"string",{:foo => "bar"},[1,5],(3..4)]
|
15
|
+
objects.each do |o|
|
16
|
+
res = Dunder.lazy_load {
|
17
|
+
o
|
18
|
+
}
|
19
|
+
assert_equal o,res._thread.value
|
20
|
+
|
21
|
+
# this works through some serious meta(monkey) programming
|
22
|
+
assert_equal res, o
|
23
|
+
assert_not_equal o,res unless [Fixnum,String,Hash,Array].include?(o.class)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
should "raise when returning forbidden objects" do
|
28
|
+
assert_raise ArgumentError do
|
29
|
+
res = Dunder.lazy_load {
|
30
|
+
:bar
|
31
|
+
}
|
32
|
+
res.inspect
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
should "duplicate objects fine" do
|
37
|
+
objects = ["string",{:foo => "bar"},[1,5]]
|
38
|
+
objects.each do |o|
|
39
|
+
res = Dunder.lazy_load {
|
40
|
+
o
|
41
|
+
}
|
42
|
+
assert_equal o,res.dup
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
20
47
|
should "respond to dunder_load" do
|
21
|
-
assert Object.
|
48
|
+
assert Object.methods.include?(:dunder_load)
|
49
|
+
b = Moods.new
|
50
|
+
res = b.dunder_load.sleepy
|
51
|
+
assert_equal b.sleepy,res
|
52
|
+
assert_equal b.sleepy.class,res.class
|
53
|
+
end
|
54
|
+
|
55
|
+
should "respond to methods" do
|
56
|
+
lazy_block = Dunder.lazy_load {
|
57
|
+
[]
|
58
|
+
}
|
59
|
+
assert lazy_block.respond_to?(:each)
|
60
|
+
|
22
61
|
b = "bar"
|
23
|
-
b.
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
62
|
+
lazy_method = b.dunder_load
|
63
|
+
assert lazy_method.respond_to?(:downcase)
|
64
|
+
end
|
65
|
+
|
66
|
+
should "respond to class methods" do
|
67
|
+
assert Object.dunder_load.name == Object.name
|
68
|
+
end
|
69
|
+
|
70
|
+
should "block when exiting until done" do
|
71
|
+
m = Mutex.new
|
72
|
+
m.lock
|
73
|
+
# Lazy Thread
|
74
|
+
lazy = Dunder.lazy_load {
|
75
|
+
m.lock
|
76
|
+
}
|
77
|
+
assert lazy._thread.alive?
|
78
|
+
m2 = Mutex.new
|
79
|
+
|
80
|
+
#Cleaner thread
|
81
|
+
Thread.start do
|
82
|
+
m2.lock
|
83
|
+
Dunder::Future.ensure_threads_finished(1)
|
84
|
+
m2.unlock
|
85
|
+
end
|
86
|
+
|
87
|
+
# Block until cleaner thread has started
|
88
|
+
while !m2.locked?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Let the lazy thread finish
|
92
|
+
m.unlock
|
93
|
+
|
94
|
+
# Let the cleaner wait for the lazy thread and wait for the cleaner thread to finish
|
95
|
+
m2.lock
|
96
|
+
assert !lazy._thread.alive?
|
97
|
+
end
|
98
|
+
|
99
|
+
should "be nonblocking " do
|
100
|
+
m = Mutex.new
|
101
|
+
m.lock
|
102
|
+
lazy = Dunder.lazy_load {
|
103
|
+
m.lock
|
104
|
+
m.unlock
|
105
|
+
"bar"
|
106
|
+
}
|
107
|
+
m.unlock
|
108
|
+
assert_equal "bar",lazy
|
109
|
+
end
|
110
|
+
|
111
|
+
should "still work if block finishes before access" do
|
112
|
+
m = Mutex.new
|
113
|
+
m.lock
|
114
|
+
lazy = Dunder.lazy_load {
|
115
|
+
m.lock
|
116
|
+
"bar"
|
117
|
+
}
|
118
|
+
m.unlock
|
119
|
+
while lazy._thread.alive?
|
28
120
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
121
|
+
assert lazy == "bar"
|
122
|
+
end
|
123
|
+
|
124
|
+
should "respond to rails" do
|
125
|
+
posts = Post.all
|
126
|
+
lazy = Dunder.lazy_load do
|
127
|
+
Post.all
|
34
128
|
end
|
35
|
-
|
36
|
-
|
129
|
+
|
130
|
+
assert posts == lazy
|
131
|
+
assert Post.scoped.dunder_load.all == posts
|
132
|
+
assert Post.dunder_load.all == posts
|
37
133
|
end
|
134
|
+
|
38
135
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'helper'
|
2
|
+
class TestDunderGroup < Test::Unit::TestCase
|
3
|
+
|
4
|
+
should "run threads in a group" do
|
5
|
+
g = Dunder::Group.new 1
|
6
|
+
b = "bar"
|
7
|
+
|
8
|
+
lazy = g.lazy_load {
|
9
|
+
b
|
10
|
+
}
|
11
|
+
assert lazy == b
|
12
|
+
assert g.running == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
should "be able to run just normal threads" do
|
16
|
+
g = Dunder::Group.new(2)
|
17
|
+
b = "bar"
|
18
|
+
t = g.start_thread {
|
19
|
+
b
|
20
|
+
}
|
21
|
+
assert b,t.value
|
22
|
+
end
|
23
|
+
|
24
|
+
should "queue threads in group" do
|
25
|
+
# When reading the code below it is recommended to reserve 30 min and a coffee
|
26
|
+
|
27
|
+
# Create a group with a maximum of 2 threads running
|
28
|
+
g = Dunder::Group.new 2
|
29
|
+
# Create a mutex control array [[m1,m2],[m1,m2], ... ]
|
30
|
+
array = 4.times.map do [Mutex.new,Mutex.new] end
|
31
|
+
|
32
|
+
# Lock all the second mutexes
|
33
|
+
array.map(&:second).each &:lock
|
34
|
+
|
35
|
+
# Start four jobs that try to each lock their specific locks
|
36
|
+
results = array.map do |first,second|
|
37
|
+
g.lazy_load do
|
38
|
+
first.lock
|
39
|
+
second.lock
|
40
|
+
first
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Wait until 2 of the first locks have been locked
|
45
|
+
while array.map(&:first).reject(&:locked?).length != 2 || g.waiting.length != 2
|
46
|
+
end
|
47
|
+
|
48
|
+
# Assert that only 2 of the threads are running
|
49
|
+
assert_equal 2,g.running
|
50
|
+
assert_equal 2,g.waiting.length
|
51
|
+
|
52
|
+
# Find a the second lock for one of the already locked jobs
|
53
|
+
mutex_to_unlock = nil
|
54
|
+
for first,second in array
|
55
|
+
if first.locked?
|
56
|
+
mutex_to_unlock = second
|
57
|
+
break
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Unlock the second lock and whatch how a one job finishes and another one gets scheduled
|
62
|
+
mutex_to_unlock.unlock
|
63
|
+
while g.waiting.length == 2
|
64
|
+
end
|
65
|
+
assert_equal 1,g.waiting.length
|
66
|
+
assert_equal 2,g.running
|
67
|
+
|
68
|
+
# Unlock the rest of the second locks
|
69
|
+
array.map(&:second).reject do |m|
|
70
|
+
m == mutex_to_unlock
|
71
|
+
end.each &:unlock
|
72
|
+
|
73
|
+
# Wait for all threads to finish
|
74
|
+
assert_not_equal array.map(&:first), results
|
75
|
+
assert_equal results,array.map(&:first)
|
76
|
+
assert g.waiting.length == 0
|
77
|
+
assert g.running == 0
|
78
|
+
end
|
79
|
+
|
80
|
+
should "limit number of running threads in a Group" do
|
81
|
+
g = Dunder::Group.new 1
|
82
|
+
srand 123
|
83
|
+
count = 32
|
84
|
+
enum = 10.times
|
85
|
+
result = []
|
86
|
+
array = enum.map do |i|
|
87
|
+
Dunder.lazy_load {
|
88
|
+
sleep(rand * 0.001)
|
89
|
+
result << i
|
90
|
+
i
|
91
|
+
}
|
92
|
+
end
|
93
|
+
assert_equal enum.to_a,array
|
94
|
+
# Since we are only running one thread at a time they should come in order
|
95
|
+
assert_not_equal enum.to_a,result, "Since we seeded srand this should not happen but could because of thread scheduling"
|
96
|
+
assert g.running == 0
|
97
|
+
assert g.waiting.length == 0
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dunder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 1
|
9
|
-
version: 0.2.1
|
4
|
+
prerelease:
|
5
|
+
version: 0.3.0
|
10
6
|
platform: ruby
|
11
7
|
authors:
|
12
8
|
- Fonsan
|
@@ -14,95 +10,86 @@ autorequire:
|
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
12
|
|
17
|
-
date: 2011-
|
13
|
+
date: 2011-02-16 00:00:00 +01:00
|
18
14
|
default_executable:
|
19
15
|
dependencies:
|
20
16
|
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
17
|
+
name: activerecord
|
22
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
23
19
|
none: false
|
24
20
|
requirements:
|
25
21
|
- - ">="
|
26
22
|
- !ruby/object:Gem::Version
|
27
|
-
segments:
|
28
|
-
- 3
|
29
|
-
- 0
|
30
|
-
- 3
|
31
23
|
version: 3.0.3
|
32
|
-
type: :
|
24
|
+
type: :development
|
33
25
|
prerelease: false
|
34
26
|
version_requirements: *id001
|
35
27
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
28
|
+
name: shoulda
|
37
29
|
requirement: &id002 !ruby/object:Gem::Requirement
|
38
30
|
none: false
|
39
31
|
requirements:
|
40
32
|
- - ">="
|
41
33
|
- !ruby/object:Gem::Version
|
42
|
-
|
43
|
-
|
44
|
-
- 0
|
45
|
-
- 3
|
46
|
-
version: 3.0.3
|
47
|
-
type: :runtime
|
34
|
+
version: "0"
|
35
|
+
type: :development
|
48
36
|
prerelease: false
|
49
37
|
version_requirements: *id002
|
50
38
|
- !ruby/object:Gem::Dependency
|
51
|
-
name:
|
39
|
+
name: bundler
|
52
40
|
requirement: &id003 !ruby/object:Gem::Requirement
|
53
41
|
none: false
|
54
42
|
requirements:
|
55
|
-
- -
|
43
|
+
- - ~>
|
56
44
|
- !ruby/object:Gem::Version
|
57
|
-
|
58
|
-
- 0
|
59
|
-
version: "0"
|
45
|
+
version: 1.0.0
|
60
46
|
type: :development
|
61
47
|
prerelease: false
|
62
48
|
version_requirements: *id003
|
63
49
|
- !ruby/object:Gem::Dependency
|
64
|
-
name:
|
50
|
+
name: jeweler
|
65
51
|
requirement: &id004 !ruby/object:Gem::Requirement
|
66
52
|
none: false
|
67
53
|
requirements:
|
68
54
|
- - ~>
|
69
55
|
- !ruby/object:Gem::Version
|
70
|
-
|
71
|
-
- 1
|
72
|
-
- 0
|
73
|
-
- 0
|
74
|
-
version: 1.0.0
|
56
|
+
version: 1.5.2
|
75
57
|
type: :development
|
76
58
|
prerelease: false
|
77
59
|
version_requirements: *id004
|
78
60
|
- !ruby/object:Gem::Dependency
|
79
|
-
name:
|
61
|
+
name: rcov
|
80
62
|
requirement: &id005 !ruby/object:Gem::Requirement
|
81
63
|
none: false
|
82
64
|
requirements:
|
83
|
-
- -
|
65
|
+
- - ">="
|
84
66
|
- !ruby/object:Gem::Version
|
85
|
-
|
86
|
-
- 1
|
87
|
-
- 5
|
88
|
-
- 2
|
89
|
-
version: 1.5.2
|
67
|
+
version: "0"
|
90
68
|
type: :development
|
91
69
|
prerelease: false
|
92
70
|
version_requirements: *id005
|
93
71
|
- !ruby/object:Gem::Dependency
|
94
|
-
name:
|
72
|
+
name: sqlite3
|
95
73
|
requirement: &id006 !ruby/object:Gem::Requirement
|
96
74
|
none: false
|
97
75
|
requirements:
|
98
76
|
- - ">="
|
99
77
|
- !ruby/object:Gem::Version
|
100
|
-
segments:
|
101
|
-
- 0
|
102
78
|
version: "0"
|
103
79
|
type: :development
|
104
80
|
prerelease: false
|
105
81
|
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: ruby-debug19
|
84
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: *id007
|
106
93
|
description: |-
|
107
94
|
For tasks that can be started early and evaluated late.
|
108
95
|
|
@@ -119,6 +106,7 @@ extensions: []
|
|
119
106
|
extra_rdoc_files:
|
120
107
|
- LICENSE.txt
|
121
108
|
files:
|
109
|
+
- .gemtest
|
122
110
|
- Gemfile
|
123
111
|
- Gemfile.lock
|
124
112
|
- LICENSE.txt
|
@@ -128,7 +116,9 @@ files:
|
|
128
116
|
- dunder.gemspec
|
129
117
|
- lib/dunder.rb
|
130
118
|
- test/helper.rb
|
119
|
+
- test/test.sqlite3
|
131
120
|
- test/test_dunder.rb
|
121
|
+
- test/test_dunder_group.rb
|
132
122
|
has_rdoc: true
|
133
123
|
homepage: http://github.com/Fonsan/dunder
|
134
124
|
licenses:
|
@@ -143,7 +133,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
143
133
|
requirements:
|
144
134
|
- - ">="
|
145
135
|
- !ruby/object:Gem::Version
|
146
|
-
hash:
|
136
|
+
hash: -2028411122079561584
|
147
137
|
segments:
|
148
138
|
- 0
|
149
139
|
version: "0"
|
@@ -152,16 +142,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
142
|
requirements:
|
153
143
|
- - ">="
|
154
144
|
- !ruby/object:Gem::Version
|
155
|
-
segments:
|
156
|
-
- 0
|
157
145
|
version: "0"
|
158
146
|
requirements: []
|
159
147
|
|
160
148
|
rubyforge_project:
|
161
|
-
rubygems_version: 1.
|
149
|
+
rubygems_version: 1.5.2
|
162
150
|
signing_key:
|
163
151
|
specification_version: 3
|
164
152
|
summary: A simple way of doing heavy work in a background process and when you really need the object it will block until it is done
|
165
153
|
test_files:
|
166
154
|
- test/helper.rb
|
167
155
|
- test/test_dunder.rb
|
156
|
+
- test/test_dunder_group.rb
|