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 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
- # Add dependencies to develop your gem here.
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
- Dunder
7
- =========================
8
- For tasks that can be started early and evaluated late.
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
- Typically one might want start multiple heavy tasks concurrent.
11
- 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 ex.
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
- you simply pass a block to Dunder.load and Dunder will execute this in a thread behind the scenes.
15
- When later accessing the lazy_object will block until the thread is done and has returned or if the thread is done returns the value
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
- The implementation itself relies only on the ruby standard library and is below 50 lines of code
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.load {
37
+ lazy_object = Dunder.lazy_load {
22
38
  # heavy stuff
23
39
  value
24
40
  }
25
-
26
- or through dunder_load
41
+ # Later on access lazy_object
42
+ puts lazy_object
43
+ puts lazy_object.class # => value.class
27
44
 
28
- lazy_sorted_articles = @articles.dunder_load.sort_by do |a|
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
- Read more further down
54
+ Parallel example
39
55
 
40
- lazy_foo = Dunder.load {
41
- # Simulate heavy IO
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.load {
47
- # Simulate heavy IO
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
- lazy_array = Dunder.load do
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.load do
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
- @lazy_posts = Dunder.load do
85
- Post.all
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
- @lazy_users = Dunder.load do
88
- User.all
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
- <%= @lazyposts.each do %> <- this will block until the posts have been loaded
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
- Has only been tested with 1.9.2
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.2.1
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.2.1"
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-01-28}
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/test_dunder.rb"
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.3.7}
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.add_runtime_dependency(%q<activesupport>, [">= 3.0.3"])
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
- def initialize(&block)
5
- @_thread = Thread.start(&block)
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
- __setobj__(@_thread.value) if @_thread.alive?
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
- def self.load(&block)
19
- Future.new(&block)
20
- end
21
-
22
- class Dispacter < SimpleDelegator
23
- def initialize(object)
24
- @_dunder_obj = object
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 class
28
- @_dunder_obj.class
59
+ def running
60
+ @mutex.synchronize {
61
+ @running
62
+ }
29
63
  end
30
64
 
31
- def method_missing(method_sym, *arguments,&block)
32
- Dunder.load do
33
- @_dunder_obj.send(method_sym, *arguments,&block)
34
- end
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
- end
38
-
39
- class Object
40
- def dunder_load
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
- end
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 = nil
7
- assert_nothing_raised do
8
- Timeout::timeout(0.5) do
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.public_instance_methods.index(:dunder_load)
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.instance_eval do
24
- def something_heavy
25
- yield
26
- self
27
- end
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
- res = nil
30
- assert_nothing_raised do
31
- Timeout::timeout(0.5) do
32
- res = b.dunder_load.something_heavy { sleep 1 }
33
- end
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
- assert_equal b,res
36
- assert_equal b.class,res.class
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: false
5
- segments:
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-01-28 00:00:00 +01:00
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: activesupport
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: :runtime
24
+ type: :development
33
25
  prerelease: false
34
26
  version_requirements: *id001
35
27
  - !ruby/object:Gem::Dependency
36
- name: activerecord
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
- segments:
43
- - 3
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: shoulda
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
- segments:
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: bundler
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
- segments:
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: jeweler
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
- segments:
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: rcov
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: 1083880034677177238
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.3.7
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