isono 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/Rakefile +38 -0
- data/isono.gemspec +13 -12
- data/lib/isono.rb +5 -4
- data/lib/isono/amqp_client.rb +71 -42
- data/lib/isono/logger.rb +19 -9
- data/lib/isono/manifest.rb +9 -10
- data/lib/isono/node.rb +41 -25
- data/lib/isono/node_modules/base.rb +25 -9
- data/lib/isono/node_modules/event_channel.rb +18 -7
- data/lib/isono/node_modules/job_channel.rb +20 -21
- data/lib/isono/node_modules/job_worker.rb +47 -28
- data/lib/isono/node_modules/rpc_channel.rb +46 -96
- data/lib/isono/rack/job.rb +19 -32
- data/lib/isono/runner/base.rb +150 -0
- data/lib/isono/runner/cli.rb +28 -0
- data/lib/isono/runner/rpc_server.rb +21 -53
- data/lib/isono/thread_pool.rb +24 -19
- data/lib/isono/util.rb +12 -4
- data/lib/isono/version.rb +5 -0
- data/spec/amqp_client_spec.rb +71 -0
- data/spec/event_observable_spec.rb +6 -0
- data/spec/file_channel_spec.rb +263 -0
- data/spec/job_channel_spec.rb +47 -0
- data/spec/logger_spec.rb +45 -0
- data/spec/manifest_spec.rb +43 -0
- data/spec/node_spec.rb +64 -0
- data/spec/resource_loader_spec.rb +113 -0
- data/spec/rpc_channel_spec.rb +172 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/thread_pool_spec.rb +35 -0
- data/spec/util_spec.rb +38 -0
- data/tasks/load_resource_manifest.rake +7 -0
- metadata +79 -43
- data/lib/isono/runner/agent.rb +0 -89
data/.gitignore
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
3
|
+
require 'isono/version'
|
4
|
+
|
5
|
+
|
6
|
+
task :gem do
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rake/gempackagetask'
|
9
|
+
|
10
|
+
spec = Gem::Specification.new do |s|
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.version = Isono::VERSION
|
13
|
+
s.authors = ['axsh Ltd.', 'Masahiro Fujiwara']
|
14
|
+
s.email = ['dev@axsh.net', 'm-fujiwara@axsh.net']
|
15
|
+
s.homepage = 'http://github.com/axsh/isono'
|
16
|
+
s.summary = 'Messaging and agent fabric'
|
17
|
+
s.name = 'isono'
|
18
|
+
s.require_path = 'lib'
|
19
|
+
s.required_ruby_version = '>= 1.8.7'
|
20
|
+
s.rubyforge_project = 'isono'
|
21
|
+
|
22
|
+
s.files = `git ls-files -c`.split("\n")
|
23
|
+
|
24
|
+
s.bindir='bin'
|
25
|
+
s.executables = %w(cli)
|
26
|
+
|
27
|
+
s.add_dependency "amqp", "0.7.0"
|
28
|
+
s.add_dependency "eventmachine", "1.0.0.beta.3"
|
29
|
+
s.add_dependency "statemachine", ">= 1.0.0"
|
30
|
+
s.add_dependency "log4r"
|
31
|
+
|
32
|
+
s.add_development_dependency 'bacon'
|
33
|
+
s.add_development_dependency 'rake'
|
34
|
+
end
|
35
|
+
|
36
|
+
File.open('isono.gemspec', 'w'){|f| f.write(spec.to_ruby) }
|
37
|
+
sh "gem build isono.gemspec"
|
38
|
+
end
|
data/isono.gemspec
CHANGED
@@ -2,43 +2,44 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{isono}
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "0.2.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["axsh Ltd.", "Masahiro Fujiwara"]
|
9
|
-
s.date = %q{
|
9
|
+
s.date = %q{2011-05-26}
|
10
10
|
s.default_executable = %q{cli}
|
11
11
|
s.email = ["dev@axsh.net", "m-fujiwara@axsh.net"]
|
12
12
|
s.executables = ["cli"]
|
13
|
-
s.files = ["bin/cli", "lib/ext/shellwords.rb", "lib/isono.rb", "lib/isono/
|
13
|
+
s.files = [".gitignore", "LICENSE", "NOTICE", "Rakefile", "bin/cli", "isono.gemspec", "lib/ext/shellwords.rb", "lib/isono.rb", "lib/isono/amqp_client.rb", "lib/isono/daemonize.rb", "lib/isono/event_delegate_context.rb", "lib/isono/event_observable.rb", "lib/isono/logger.rb", "lib/isono/manifest.rb", "lib/isono/messaging_client.rb", "lib/isono/models/event_log.rb", "lib/isono/models/job_state.rb", "lib/isono/models/node_state.rb", "lib/isono/models/resource_instance.rb", "lib/isono/node.rb", "lib/isono/node_modules/base.rb", "lib/isono/node_modules/data_store.rb", "lib/isono/node_modules/event_channel.rb", "lib/isono/node_modules/event_logger.rb", "lib/isono/node_modules/job_channel.rb", "lib/isono/node_modules/job_collector.rb", "lib/isono/node_modules/job_worker.rb", "lib/isono/node_modules/node_collector.rb", "lib/isono/node_modules/node_heartbeat.rb", "lib/isono/node_modules/rpc_channel.rb", "lib/isono/rack.rb", "lib/isono/rack/builder.rb", "lib/isono/rack/data_store.rb", "lib/isono/rack/job.rb", "lib/isono/rack/map.rb", "lib/isono/rack/object_method.rb", "lib/isono/rack/proc.rb", "lib/isono/rack/thread_pass.rb", "lib/isono/resource_manifest.rb", "lib/isono/runner/base.rb", "lib/isono/runner/cli.rb", "lib/isono/runner/rpc_server.rb", "lib/isono/serializer.rb", "lib/isono/thread_pool.rb", "lib/isono/util.rb", "lib/isono/version.rb", "spec/amqp_client_spec.rb", "spec/event_observable_spec.rb", "spec/file_channel_spec.rb", "spec/job_channel_spec.rb", "spec/logger_spec.rb", "spec/manifest_spec.rb", "spec/node_spec.rb", "spec/resource_loader_spec.rb", "spec/rpc_channel_spec.rb", "spec/spec_helper.rb", "spec/thread_pool_spec.rb", "spec/util_spec.rb", "tasks/load_resource_manifest.rake"]
|
14
14
|
s.homepage = %q{http://github.com/axsh/isono}
|
15
15
|
s.require_paths = ["lib"]
|
16
16
|
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
17
|
-
s.
|
18
|
-
s.
|
17
|
+
s.rubyforge_project = %q{isono}
|
18
|
+
s.rubygems_version = %q{1.3.7}
|
19
|
+
s.summary = %q{Messaging and agent fabric}
|
19
20
|
|
20
21
|
if s.respond_to? :specification_version then
|
21
22
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
22
23
|
s.specification_version = 3
|
23
24
|
|
24
|
-
if Gem::Version.new(Gem::
|
25
|
-
s.add_runtime_dependency(%q<amqp>, ["
|
26
|
-
s.add_runtime_dependency(%q<eventmachine>, ["
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency(%q<amqp>, ["= 0.7.0"])
|
27
|
+
s.add_runtime_dependency(%q<eventmachine>, ["= 1.0.0.beta.3"])
|
27
28
|
s.add_runtime_dependency(%q<statemachine>, [">= 1.0.0"])
|
28
29
|
s.add_runtime_dependency(%q<log4r>, [">= 0"])
|
29
30
|
s.add_development_dependency(%q<bacon>, [">= 0"])
|
30
31
|
s.add_development_dependency(%q<rake>, [">= 0"])
|
31
32
|
else
|
32
|
-
s.add_dependency(%q<amqp>, ["
|
33
|
-
s.add_dependency(%q<eventmachine>, ["
|
33
|
+
s.add_dependency(%q<amqp>, ["= 0.7.0"])
|
34
|
+
s.add_dependency(%q<eventmachine>, ["= 1.0.0.beta.3"])
|
34
35
|
s.add_dependency(%q<statemachine>, [">= 1.0.0"])
|
35
36
|
s.add_dependency(%q<log4r>, [">= 0"])
|
36
37
|
s.add_dependency(%q<bacon>, [">= 0"])
|
37
38
|
s.add_dependency(%q<rake>, [">= 0"])
|
38
39
|
end
|
39
40
|
else
|
40
|
-
s.add_dependency(%q<amqp>, ["
|
41
|
-
s.add_dependency(%q<eventmachine>, ["
|
41
|
+
s.add_dependency(%q<amqp>, ["= 0.7.0"])
|
42
|
+
s.add_dependency(%q<eventmachine>, ["= 1.0.0.beta.3"])
|
42
43
|
s.add_dependency(%q<statemachine>, [">= 1.0.0"])
|
43
44
|
s.add_dependency(%q<log4r>, [">= 0"])
|
44
45
|
s.add_dependency(%q<bacon>, [">= 0"])
|
data/lib/isono.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
module Isono
|
4
|
-
|
5
|
-
|
4
|
+
require 'isono/version'
|
5
|
+
require 'isono/logger'
|
6
|
+
|
6
7
|
autoload :Node, 'isono/node'
|
7
8
|
autoload :AmqpClient, 'isono/amqp_client'
|
8
9
|
autoload :Daemonize, 'isono/daemonize'
|
9
10
|
autoload :Util, 'isono/util'
|
10
11
|
autoload :ThreadPool, 'isono/thread_pool'
|
11
|
-
autoload :Logger, 'isono/logger'
|
12
12
|
autoload :Manifest, 'isono/manifest'
|
13
13
|
autoload :Serializer, 'isono/serializer'
|
14
14
|
autoload :EventObservable, 'isono/event_observable'
|
@@ -28,7 +28,8 @@ module Isono
|
|
28
28
|
autoload :JobCollector, 'isono/node_modules/job_collector'
|
29
29
|
end
|
30
30
|
module Runner
|
31
|
-
autoload :
|
31
|
+
autoload :Base, 'isono/runner/base'
|
32
|
+
autoload :CLI, 'isono/runner/cli'
|
32
33
|
autoload :RpcServer, 'isono/runner/rpc_server'
|
33
34
|
end
|
34
35
|
module Rack
|
data/lib/isono/amqp_client.rb
CHANGED
@@ -71,26 +71,35 @@ module Isono
|
|
71
71
|
:pass=>broker_uri.password ||default[:pass]
|
72
72
|
}
|
73
73
|
opts.merge!(args) if args.is_a?(Hash)
|
74
|
-
|
75
|
-
@amqp_client = ::AMQP.connect(opts)
|
76
|
-
@amqp_client.instance_eval {
|
77
|
-
def settings
|
78
|
-
@settings
|
79
|
-
end
|
80
|
-
}
|
81
|
-
@amqp_client.callback {
|
82
|
-
on_connect
|
83
|
-
if blk
|
84
|
-
blk.arity == 1 ? blk.call(:success) : blk.call
|
85
|
-
end
|
86
|
-
}
|
87
|
-
@amqp_client.errback {
|
88
|
-
logger.error("Failed to connect to the broker: #{amqp_server_uri}")
|
89
|
-
blk.call(:error) if blk && blk.arity == 1
|
90
|
-
}
|
91
|
-
# Note: Thread.current[:mq] is utilized in amqp gem.
|
92
|
-
Thread.current[:mq] = ::MQ.new(@amqp_client)
|
93
74
|
|
75
|
+
prepare_connect {
|
76
|
+
@amqp_client = ::AMQP.connect(opts)
|
77
|
+
@amqp_client.instance_eval {
|
78
|
+
def settings
|
79
|
+
@settings
|
80
|
+
end
|
81
|
+
}
|
82
|
+
@amqp_client.connection_status { |t|
|
83
|
+
case t
|
84
|
+
when :connected
|
85
|
+
# here is tried also when reconnected
|
86
|
+
on_connect
|
87
|
+
when :disconnected
|
88
|
+
on_disconnected
|
89
|
+
end
|
90
|
+
}
|
91
|
+
# the block argument is called once at the initial connection.
|
92
|
+
@amqp_client.callback {
|
93
|
+
after_connect
|
94
|
+
if blk
|
95
|
+
blk.arity == 1 ? blk.call(self) : blk.call
|
96
|
+
end
|
97
|
+
}
|
98
|
+
@amqp_client.errback {
|
99
|
+
logger.error("Failed to connect to the broker: #{amqp_server_uri}")
|
100
|
+
blk.call(self) if blk && blk.arity == 1
|
101
|
+
}
|
102
|
+
}
|
94
103
|
self
|
95
104
|
end
|
96
105
|
|
@@ -105,24 +114,50 @@ module Isono
|
|
105
114
|
|
106
115
|
def on_connect
|
107
116
|
end
|
108
|
-
|
117
|
+
|
118
|
+
def on_disconnected
|
119
|
+
end
|
120
|
+
|
109
121
|
def on_close
|
110
122
|
end
|
111
123
|
|
124
|
+
def before_connect
|
125
|
+
end
|
126
|
+
|
127
|
+
def after_connect
|
128
|
+
end
|
129
|
+
|
130
|
+
def before_close
|
131
|
+
end
|
132
|
+
|
133
|
+
def after_close
|
134
|
+
end
|
135
|
+
|
112
136
|
def close(&blk)
|
113
137
|
return unless connected?
|
114
138
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
139
|
+
prepare_close {
|
140
|
+
@amqp_client.close {
|
141
|
+
begin
|
142
|
+
on_close
|
143
|
+
after_close
|
144
|
+
blk.call if blk
|
145
|
+
ensure
|
146
|
+
@amqp_client = nil
|
147
|
+
Thread.current[:mq] = nil
|
148
|
+
end
|
149
|
+
}
|
123
150
|
}
|
124
151
|
end
|
125
152
|
|
153
|
+
# Create new AMQP channel object
|
154
|
+
#
|
155
|
+
# @note Do not have to close by user. Channel close is performed
|
156
|
+
# as part of connection close.
|
157
|
+
def create_channel
|
158
|
+
MQ.new(@amqp_client)
|
159
|
+
end
|
160
|
+
|
126
161
|
# Publish a message to the designated exchange.
|
127
162
|
#
|
128
163
|
# @param [String] exname The exchange name
|
@@ -148,22 +183,16 @@ module Isono
|
|
148
183
|
}
|
149
184
|
end
|
150
185
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
186
|
+
private
|
187
|
+
def prepare_connect(&blk)
|
188
|
+
before_connect
|
189
|
+
blk.call
|
155
190
|
end
|
156
191
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
begin
|
161
|
-
define_queue("ident.#{unique_id}", "identity", {:exclusive=>true, :nowait=>false})
|
162
|
-
rescue MQ::Error => e
|
163
|
-
logger.error("The node having same ID already exists: #{unique_id}")
|
164
|
-
raise e
|
165
|
-
end
|
192
|
+
def prepare_close(&blk)
|
193
|
+
before_close
|
194
|
+
blk.call
|
166
195
|
end
|
167
|
-
|
196
|
+
|
168
197
|
end
|
169
198
|
end
|
data/lib/isono/logger.rb
CHANGED
@@ -6,16 +6,18 @@ module Isono
|
|
6
6
|
# Injects +logger+ method to the included class.
|
7
7
|
# The output message from the logger methods starts the module name trailing message body.
|
8
8
|
module Logger
|
9
|
+
@rootlogger = Log4r::Logger.new('Isono')
|
10
|
+
|
11
|
+
def self.initialize(l4r_output=Log4r::StdoutOutputter.new('stdout'))
|
12
|
+
# Isono top level logger
|
13
|
+
formatter = Log4r::PatternFormatter.new(:depth => 9999, # stack trace depth
|
14
|
+
:pattern => "%d %c [%l]: %M",
|
15
|
+
:date_format => "%Y/%m/%d %H:%M:%S"
|
16
|
+
)
|
17
|
+
l4r_output.formatter = formatter
|
18
|
+
@rootlogger.outputters = l4r_output
|
19
|
+
end
|
9
20
|
|
10
|
-
# Isono top level logger
|
11
|
-
rootlog = Log4r::Logger.new('Isono')
|
12
|
-
formatter = Log4r::PatternFormatter.new(:depth => 9999, # stack trace depth
|
13
|
-
:pattern => "%d %c [%l]: %M",
|
14
|
-
:date_format => "%Y/%m/%d %H:%M:%S"
|
15
|
-
)
|
16
|
-
rootlog.add(Log4r::StdoutOutputter.new('stdout', :formatter => formatter))
|
17
|
-
|
18
|
-
|
19
21
|
def self.included(klass)
|
20
22
|
klass.class_eval {
|
21
23
|
|
@@ -45,4 +47,12 @@ module Isono
|
|
45
47
|
end
|
46
48
|
|
47
49
|
end
|
50
|
+
|
51
|
+
# Set STDOUT as the default log output.
|
52
|
+
# To replace another log device, put the line below at the top of
|
53
|
+
# your code:
|
54
|
+
# Isono::Logger.initialize(Log4r::SyslogOutputter.new('mysyslog'))
|
55
|
+
# To disable any of log output:
|
56
|
+
# Isono::Logger.initialize(Log4r::Outputter.new('null'))
|
57
|
+
Logger.initialize
|
48
58
|
end
|
data/lib/isono/manifest.rb
CHANGED
@@ -25,14 +25,15 @@ module Isono
|
|
25
25
|
|
26
26
|
# @param [String] app_root Application root folder
|
27
27
|
# @param [block]
|
28
|
-
def initialize(app_root, &blk)
|
28
|
+
def initialize(app_root='.', &blk)
|
29
29
|
@node_modules = []
|
30
30
|
resolve_abs_app_root(app_root)
|
31
31
|
@config = ConfigStruct.new
|
32
32
|
@config.app_root = app_root
|
33
33
|
|
34
34
|
instance_eval(&blk) if blk
|
35
|
-
|
35
|
+
|
36
|
+
load_config(@config_path) if @config_path
|
36
37
|
end
|
37
38
|
|
38
39
|
# Regist a node module class to be initialized/terminated.
|
@@ -65,7 +66,7 @@ module Isono
|
|
65
66
|
end
|
66
67
|
|
67
68
|
def node_id
|
68
|
-
"#{@node_name}
|
69
|
+
"#{@node_name}.#{@node_instance_id}"
|
69
70
|
end
|
70
71
|
|
71
72
|
def config_path(path=nil)
|
@@ -79,18 +80,16 @@ module Isono
|
|
79
80
|
end
|
80
81
|
@config
|
81
82
|
end
|
82
|
-
|
83
83
|
|
84
|
-
private
|
85
84
|
# load config file and merge up with the config tree.
|
86
85
|
# it will not work when the config_path is nil or the file is missed
|
87
|
-
def load_config
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
86
|
+
def load_config(path)
|
87
|
+
return unless File.exist?(path)
|
88
|
+
buf = File.read(path)
|
89
|
+
eval("#{buf}", binding, path)
|
92
90
|
end
|
93
91
|
|
92
|
+
private
|
94
93
|
def resolve_abs_app_root(app_root_path)
|
95
94
|
pt = Pathname.new(app_root_path)
|
96
95
|
if pt.absolute?
|
data/lib/isono/node.rb
CHANGED
@@ -12,6 +12,7 @@ module Isono
|
|
12
12
|
include EventObservable
|
13
13
|
|
14
14
|
def self.inherited(klass)
|
15
|
+
super
|
15
16
|
klass.class_eval {
|
16
17
|
include Logger
|
17
18
|
}
|
@@ -66,46 +67,61 @@ module Isono
|
|
66
67
|
manifest.node_id
|
67
68
|
end
|
68
69
|
|
69
|
-
def
|
70
|
+
def before_connect
|
70
71
|
raise "node_id is not set" if node_id.nil?
|
71
72
|
|
72
|
-
|
73
|
-
identity_queue(node_id)
|
74
|
-
init_modules
|
75
|
-
|
76
|
-
fire_event(:node_ready, {:node_id=> self.node_id})
|
77
|
-
logger.info("Started : AMQP Server=#{amqp_server_uri.to_s}, ID=#{node_id}, token=#{boot_token}")
|
78
|
-
end
|
79
|
-
|
80
|
-
def on_close
|
81
|
-
term_modules
|
82
|
-
end
|
73
|
+
@value_objects = {}
|
83
74
|
|
84
|
-
private
|
85
|
-
|
86
|
-
def init_modules
|
87
75
|
manifest.node_modules.each { |modclass, *args|
|
88
76
|
if !@value_objects.has_key?(modclass)
|
89
77
|
@value_objects[modclass] = vo = ValueObject.new(self, modclass)
|
90
78
|
|
91
|
-
|
92
|
-
vo.instance_eval(&modclass.initialize_hook)
|
93
|
-
end
|
94
|
-
|
95
|
-
logger.debug("Initialized #{modclass.to_s}")
|
79
|
+
node_hook_proc(:before_connect).call(modclass, *args)
|
96
80
|
end
|
97
81
|
}
|
98
82
|
end
|
99
83
|
|
100
|
-
def
|
101
|
-
|
84
|
+
def after_connect
|
85
|
+
setup_identity_queue
|
86
|
+
|
87
|
+
manifest.node_modules.each &node_hook_proc(:after_connect)
|
88
|
+
# TODO: remove initialize_hook
|
89
|
+
manifest.node_modules.each &node_hook_proc(:initialize)
|
90
|
+
|
91
|
+
logger.info("Started : AMQP Server=#{amqp_server_uri.to_s}, ID=#{node_id}, token=#{boot_token}")
|
92
|
+
end
|
93
|
+
|
94
|
+
def before_close
|
95
|
+
manifest.node_modules.reverse.each &node_hook_proc(:before_close)
|
96
|
+
# TODO: remove terminate_hook
|
97
|
+
manifest.node_modules.reverse.each &node_hook_proc(:terminate)
|
98
|
+
end
|
99
|
+
|
100
|
+
def after_close
|
101
|
+
manifest.node_modules.reverse.each &node_hook_proc(:after_close)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def node_hook_proc(hook)
|
107
|
+
proc { |modclass, *args|
|
108
|
+
node_hook = modclass.node_hooks[hook]
|
109
|
+
next unless node_hook.is_a?(Proc)
|
102
110
|
vo = @value_objects[modclass]
|
103
|
-
if vo
|
104
|
-
vo.instance_eval(&
|
111
|
+
if vo
|
112
|
+
vo.instance_eval(&node_hook)
|
105
113
|
end
|
106
|
-
logger.info("Terminated #{modclass.to_s}")
|
107
114
|
}
|
108
115
|
end
|
116
|
+
|
117
|
+
def setup_identity_queue
|
118
|
+
amq = create_channel
|
119
|
+
amq.errback {
|
120
|
+
logger.error("The node has same node ID is running already")
|
121
|
+
exit(1)
|
122
|
+
}
|
123
|
+
amq.queue("ident.#{node_id}", {:exclusive=>true})
|
124
|
+
end
|
109
125
|
|
110
126
|
class ValueObject
|
111
127
|
module DelegateMethods
|