skylight-core 2.0.0.beta1

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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/lib/skylight/core/config.rb +454 -0
  3. data/lib/skylight/core/errors.rb +6 -0
  4. data/lib/skylight/core/fanout.rb +44 -0
  5. data/lib/skylight/core/formatters/http.rb +23 -0
  6. data/lib/skylight/core/gc.rb +107 -0
  7. data/lib/skylight/core/instrumentable.rb +144 -0
  8. data/lib/skylight/core/instrumenter.rb +249 -0
  9. data/lib/skylight/core/middleware.rb +101 -0
  10. data/lib/skylight/core/normalizers/action_controller/process_action.rb +50 -0
  11. data/lib/skylight/core/normalizers/action_controller/send_file.rb +50 -0
  12. data/lib/skylight/core/normalizers/action_view/render_collection.rb +22 -0
  13. data/lib/skylight/core/normalizers/action_view/render_partial.rb +21 -0
  14. data/lib/skylight/core/normalizers/action_view/render_template.rb +21 -0
  15. data/lib/skylight/core/normalizers/active_job/enqueue_at.rb +21 -0
  16. data/lib/skylight/core/normalizers/active_model_serializers/render.rb +26 -0
  17. data/lib/skylight/core/normalizers/active_record/instantiation.rb +17 -0
  18. data/lib/skylight/core/normalizers/active_record/sql.rb +33 -0
  19. data/lib/skylight/core/normalizers/active_support/cache.rb +20 -0
  20. data/lib/skylight/core/normalizers/active_support/cache_clear.rb +16 -0
  21. data/lib/skylight/core/normalizers/active_support/cache_decrement.rb +16 -0
  22. data/lib/skylight/core/normalizers/active_support/cache_delete.rb +16 -0
  23. data/lib/skylight/core/normalizers/active_support/cache_exist.rb +16 -0
  24. data/lib/skylight/core/normalizers/active_support/cache_fetch_hit.rb +16 -0
  25. data/lib/skylight/core/normalizers/active_support/cache_generate.rb +16 -0
  26. data/lib/skylight/core/normalizers/active_support/cache_increment.rb +16 -0
  27. data/lib/skylight/core/normalizers/active_support/cache_read.rb +16 -0
  28. data/lib/skylight/core/normalizers/active_support/cache_read_multi.rb +16 -0
  29. data/lib/skylight/core/normalizers/active_support/cache_write.rb +16 -0
  30. data/lib/skylight/core/normalizers/coach/handler_finish.rb +36 -0
  31. data/lib/skylight/core/normalizers/coach/middleware_finish.rb +23 -0
  32. data/lib/skylight/core/normalizers/couch_potato/query.rb +20 -0
  33. data/lib/skylight/core/normalizers/data_mapper/sql.rb +12 -0
  34. data/lib/skylight/core/normalizers/default.rb +27 -0
  35. data/lib/skylight/core/normalizers/elasticsearch/request.rb +20 -0
  36. data/lib/skylight/core/normalizers/faraday/request.rb +37 -0
  37. data/lib/skylight/core/normalizers/grape/endpoint.rb +30 -0
  38. data/lib/skylight/core/normalizers/grape/endpoint_render.rb +26 -0
  39. data/lib/skylight/core/normalizers/grape/endpoint_run.rb +33 -0
  40. data/lib/skylight/core/normalizers/grape/endpoint_run_filters.rb +23 -0
  41. data/lib/skylight/core/normalizers/moped/query.rb +100 -0
  42. data/lib/skylight/core/normalizers/sequel/sql.rb +12 -0
  43. data/lib/skylight/core/normalizers/sql.rb +49 -0
  44. data/lib/skylight/core/normalizers.rb +170 -0
  45. data/lib/skylight/core/probes/action_controller.rb +31 -0
  46. data/lib/skylight/core/probes/action_view.rb +37 -0
  47. data/lib/skylight/core/probes/active_model_serializers.rb +55 -0
  48. data/lib/skylight/core/probes/elasticsearch.rb +37 -0
  49. data/lib/skylight/core/probes/excon/middleware.rb +72 -0
  50. data/lib/skylight/core/probes/excon.rb +26 -0
  51. data/lib/skylight/core/probes/faraday.rb +22 -0
  52. data/lib/skylight/core/probes/grape.rb +80 -0
  53. data/lib/skylight/core/probes/httpclient.rb +46 -0
  54. data/lib/skylight/core/probes/middleware.rb +58 -0
  55. data/lib/skylight/core/probes/mongo.rb +171 -0
  56. data/lib/skylight/core/probes/mongoid.rb +21 -0
  57. data/lib/skylight/core/probes/moped.rb +39 -0
  58. data/lib/skylight/core/probes/net_http.rb +64 -0
  59. data/lib/skylight/core/probes/redis.rb +71 -0
  60. data/lib/skylight/core/probes/sequel.rb +33 -0
  61. data/lib/skylight/core/probes/sinatra.rb +69 -0
  62. data/lib/skylight/core/probes/tilt.rb +27 -0
  63. data/lib/skylight/core/probes.rb +129 -0
  64. data/lib/skylight/core/railtie.rb +166 -0
  65. data/lib/skylight/core/subscriber.rb +124 -0
  66. data/lib/skylight/core/test.rb +98 -0
  67. data/lib/skylight/core/trace.rb +190 -0
  68. data/lib/skylight/core/user_config.rb +61 -0
  69. data/lib/skylight/core/util/allocation_free.rb +26 -0
  70. data/lib/skylight/core/util/clock.rb +56 -0
  71. data/lib/skylight/core/util/deploy.rb +132 -0
  72. data/lib/skylight/core/util/gzip.rb +21 -0
  73. data/lib/skylight/core/util/inflector.rb +112 -0
  74. data/lib/skylight/core/util/logging.rb +127 -0
  75. data/lib/skylight/core/util/platform.rb +77 -0
  76. data/lib/skylight/core/util/proxy.rb +13 -0
  77. data/lib/skylight/core/util.rb +14 -0
  78. data/lib/skylight/core/vendor/active_support/notifications.rb +207 -0
  79. data/lib/skylight/core/vendor/active_support/per_thread_registry.rb +52 -0
  80. data/lib/skylight/core/vendor/thread_safe/non_concurrent_cache_backend.rb +133 -0
  81. data/lib/skylight/core/vendor/thread_safe/synchronized_cache_backend.rb +76 -0
  82. data/lib/skylight/core/vendor/thread_safe.rb +126 -0
  83. data/lib/skylight/core/version.rb +6 -0
  84. data/lib/skylight/core/vm/gc.rb +70 -0
  85. data/lib/skylight/core.rb +99 -0
  86. metadata +254 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5f2101f565718179a58601c833bd3005ce789b2159c689dcf4a68f02ee4ead8e
4
+ data.tar.gz: 34f6391c7691bdb47b33bef9dc0fddc3808d98d82eb809adaebe3dfb4d4a6aa6
5
+ SHA512:
6
+ metadata.gz: f867502b872f1604375b24a7a4591c4d733f38327b1d985a7bd8a5d2b64cff5a7fa59482357d88fc7fd0e1314c7200f054f448df07e398c1c14aa00e0bcb148c
7
+ data.tar.gz: b021ff2ed79a9f52cd9eaab91a3377547318195e6c7a2a11fed443565527245029b85970720fd9ef573064fe3443743339fc10ed4d8eb4cb6b2056fb68a9879b
@@ -0,0 +1,454 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'thread'
4
+ require 'erb'
5
+ require 'json'
6
+ require 'skylight/core/util/logging'
7
+ require 'skylight/core/util/proxy'
8
+ require 'skylight/core/errors'
9
+
10
+ module Skylight::Core
11
+ class Config
12
+ include Util::Logging
13
+
14
+ # @api private
15
+ MUTEX = Mutex.new
16
+
17
+ def self.env_matcher; /^(?:SK|SKYLIGHT)_(.+)$/ end
18
+ def self.native_env_prefix; "SKYLIGHT_" end
19
+
20
+ # Map environment variable keys with Skylight configuration keys
21
+ def self.env_to_key
22
+ {
23
+ # == Logging ==
24
+ 'LOG_FILE' => :log_file,
25
+ 'LOG_LEVEL' => :log_level,
26
+ 'ALERT_LOG_FILE' => :alert_log_file,
27
+ 'LOG_SQL_PARSE_ERRORS' => :log_sql_parse_errors,
28
+
29
+ # == Proxy ==
30
+ 'PROXY_URL' => :proxy_url,
31
+
32
+ # == Instrumenter ==
33
+ "ENABLE_SEGMENTS" => :enable_segments,
34
+
35
+ # == User config settings ==
36
+ "USER_CONFIG_PATH" => :'user_config_path',
37
+
38
+ # == Heroku settings ==
39
+ #
40
+ "HEROKU_DYNO_INFO_PATH" => :'heroku.dyno_info_path'
41
+ }
42
+ end
43
+
44
+ # Default values for Skylight configuration keys
45
+ def self.default_values
46
+ {
47
+ :log_file => '-'.freeze,
48
+ :log_level => 'INFO'.freeze,
49
+ :alert_log_file => '-'.freeze,
50
+ :log_sql_parse_errors => false,
51
+ :enable_segments => true,
52
+ :'heroku.dyno_info_path' => '/etc/heroku/dyno'
53
+ }
54
+ end
55
+
56
+ def self.required_keys
57
+ # Nothing is required in this base class.
58
+ {}
59
+ end
60
+
61
+ def self.server_validated_keys
62
+ # Nothing is validated for now, but this is a list of symbols
63
+ # for the key we want to validate.
64
+ []
65
+ end
66
+
67
+ def self.native_env_keys
68
+ [
69
+ :version,
70
+ :root,
71
+ :proxy_url
72
+ ]
73
+ end
74
+
75
+ # Maps legacy config keys to new config keys
76
+ def self.legacy_keys
77
+ # No legacy keys for now
78
+ {}
79
+ end
80
+
81
+ def self.validators
82
+ # None for now
83
+ {}
84
+ end
85
+
86
+ # @api private
87
+ attr_reader :environment
88
+
89
+ # @api private
90
+ def initialize(*args)
91
+ attrs = {}
92
+
93
+ if Hash === args.last
94
+ attrs = args.pop.dup
95
+ end
96
+
97
+ @values = {}
98
+ @priority = {}
99
+ @regexp = nil
100
+
101
+ p = attrs.delete(:priority)
102
+
103
+ if @environment = args[0]
104
+ @regexp = /^#{Regexp.escape(@environment)}\.(.+)$/
105
+ end
106
+
107
+ attrs.each do |k, v|
108
+ self[k] = v
109
+ end
110
+
111
+ if p
112
+ p.each do |k, v|
113
+ @priority[self.class.remap_key(k)] = v
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.load(opts = {}, env = ENV)
119
+ attrs = {}
120
+ version = nil
121
+
122
+ path = opts.delete(:file)
123
+ environment = opts.delete(:environment)
124
+
125
+ if path
126
+ error = nil
127
+ begin
128
+ attrs = YAML.load(ERB.new(File.read(path)).result)
129
+ error = "empty file" unless attrs
130
+ error = "invalid format" if attrs && !attrs.is_a?(Hash)
131
+ rescue Exception => e
132
+ error = e.message
133
+ end
134
+
135
+ raise ConfigError, "could not load config file; msg=#{error}" if error
136
+
137
+ version = File.mtime(path).to_i
138
+ end
139
+
140
+ if env
141
+ attrs[:priority] = remap_env(env)
142
+ end
143
+
144
+ config = new(environment, attrs)
145
+
146
+ opts.each do |k, v|
147
+ config[k] = v
148
+ end
149
+
150
+ config
151
+ end
152
+
153
+ def self.remap_key(key)
154
+ key = key.to_sym
155
+ legacy_keys[key] || key
156
+ end
157
+
158
+ # @api private
159
+ def self.remap_env(env)
160
+ ret = {}
161
+
162
+ return ret unless env
163
+
164
+ # Only set if it exists, we don't want to set to a nil value
165
+ if proxy_url = Util::Proxy.detect_url(env)
166
+ ret[:proxy_url] = proxy_url
167
+ end
168
+
169
+ env.each do |k, val|
170
+ next unless k =~ env_matcher
171
+
172
+ if key = env_to_key[$1]
173
+ ret[key] =
174
+ case val
175
+ when /^false$/i then false
176
+ when /^true$/i then true
177
+ when /^(nil|null)$/i then nil
178
+ when /^\d+$/ then val.to_i
179
+ when /^\d+\.\d+$/ then val.to_f
180
+ else val
181
+ end
182
+ end
183
+ end
184
+
185
+ ret
186
+ end
187
+
188
+ # @api private
189
+ def validate!
190
+ self.class.required_keys.each do |k, v|
191
+ unless get(k)
192
+ raise ConfigError, "#{v} required"
193
+ end
194
+ end
195
+
196
+ log_file = self[:log_file]
197
+ alert_log_file = self[:alert_log_file]
198
+
199
+ check_logfile_permissions(log_file, "log_file")
200
+ check_logfile_permissions(alert_log_file, "alert_log_file")
201
+
202
+ true
203
+ end
204
+
205
+ def validate_with_server
206
+ true
207
+ end
208
+
209
+ def check_file_permissions(file, key)
210
+ file_root = File.dirname(file)
211
+
212
+ # Try to make the directory, don't blow up if we can't. Our writable? check will fail later.
213
+ FileUtils.mkdir_p file_root rescue nil
214
+
215
+ if File.exist?(file) && !FileTest.writable?(file)
216
+ raise ConfigError, "File `#{file}` is not writable. Please set #{key} in your config to a writable path"
217
+ end
218
+
219
+ unless FileTest.writable?(file_root)
220
+ raise ConfigError, "Directory `#{file_root}` is not writable. Please set #{key} in your config to a writable path"
221
+ end
222
+ end
223
+
224
+ def check_logfile_permissions(log_file, key)
225
+ return if log_file == '-' # STDOUT
226
+ log_file = File.expand_path(log_file, root)
227
+ check_file_permissions(log_file, key)
228
+ end
229
+
230
+ def key?(key)
231
+ key = self.class.remap_key(key)
232
+ @priority.key?(key) || @values.key?(key)
233
+ end
234
+
235
+ def get(key, default = nil, &blk)
236
+ key = self.class.remap_key(key)
237
+
238
+ return @priority[key] if @priority.key?(key)
239
+ return @values[key] if @values.key?(key)
240
+ return self.class.default_values[key] if self.class.default_values.key?(key)
241
+
242
+ if default
243
+ return default
244
+ elsif blk
245
+ return blk.call(key)
246
+ end
247
+
248
+ nil
249
+ end
250
+
251
+ alias [] get
252
+
253
+ def set(key, val, scope = nil)
254
+ if scope
255
+ key = [scope, key].join('.')
256
+ end
257
+
258
+ if Hash === val
259
+ val.each do |k, v|
260
+ set(k, v, key)
261
+ end
262
+ else
263
+ k = self.class.remap_key(key)
264
+
265
+ if validator = self.class.validators[k]
266
+ blk, msg = validator
267
+
268
+ unless blk.call(val, self)
269
+ error_msg = "invalid value for #{k} (#{val})"
270
+ error_msg << ", #{msg}" if msg
271
+ raise ConfigError, error_msg
272
+ end
273
+ end
274
+
275
+ if @regexp && k =~ @regexp
276
+ @priority[$1.to_sym] = val
277
+ end
278
+
279
+ @values[k] = val
280
+ end
281
+ end
282
+
283
+ alias []= set
284
+
285
+ def send_or_get(v)
286
+ respond_to?(v) ? send(v) : get(v)
287
+ end
288
+
289
+ def duration_ms(key, default = nil)
290
+ if (v = self[key]) && v.to_s =~ /^\s*(\d+)(s|sec|ms|micros|nanos)?\s*$/
291
+ v = $1.to_i
292
+ case $2
293
+ when "ms"
294
+ v
295
+ when "micros"
296
+ v / 1_000
297
+ when "nanos"
298
+ v / 1_000_000
299
+ else # "s", "sec", nil
300
+ v * 1000
301
+ end
302
+ else
303
+ default
304
+ end
305
+ end
306
+
307
+ def to_json
308
+ JSON.generate(
309
+ config: {
310
+ priority: @priority,
311
+ values: @values
312
+ }
313
+ )
314
+ end
315
+
316
+ def to_native_env
317
+ ret = []
318
+
319
+ self.class.native_env_keys.each do |key|
320
+ value = send_or_get(key)
321
+ unless value.nil?
322
+ env_key = self.class.env_to_key.key(key) || key.upcase
323
+ ret << "#{self.class.native_env_prefix}#{env_key}" << cast_for_env(value)
324
+ end
325
+ end
326
+
327
+ ret
328
+ end
329
+
330
+ def write(path)
331
+ raise "not implemented"
332
+ end
333
+
334
+ #
335
+ #
336
+ # ===== Helpers =====
337
+ #
338
+ #
339
+
340
+ def version
341
+ VERSION
342
+ end
343
+
344
+ # @api private
345
+ def gc
346
+ @gc ||= GC.new(self, get('gc.profiler', VM::GC.new))
347
+ end
348
+
349
+ # @api private
350
+ def ignored_endpoints
351
+ @ignored_endpoints ||=
352
+ begin
353
+ ignored_endpoints = get(:ignored_endpoints)
354
+
355
+ # If, for some odd reason you have a comma in your endpoint name, use the
356
+ # YML config instead.
357
+ if ignored_endpoints.is_a?(String)
358
+ ignored_endpoints = ignored_endpoints.split(/\s*,\s*/)
359
+ end
360
+
361
+ val = Array(get(:ignored_endpoint))
362
+ val.concat(Array(ignored_endpoints))
363
+ val
364
+ end
365
+ end
366
+
367
+ def root
368
+ self[:root] || Dir.pwd
369
+ end
370
+
371
+ def logger
372
+ @logger ||=
373
+ MUTEX.synchronize do
374
+ load_logger
375
+ end
376
+ end
377
+
378
+ def logger=(logger)
379
+ @logger = logger
380
+ end
381
+
382
+ def alert_logger
383
+ @alert_logger ||= MUTEX.synchronize do
384
+ unless l = @alert_logger
385
+ out = get(:alert_log_file)
386
+ out = Util::AlertLogger.new(load_logger) if out == '-'
387
+
388
+ l = create_logger(out)
389
+ l.level = Logger::DEBUG
390
+ end
391
+
392
+ l
393
+ end
394
+ end
395
+
396
+ def alert_logger=(logger)
397
+ @alert_logger = logger
398
+ end
399
+
400
+ def enable_segments?
401
+ !!get(:enable_segments)
402
+ end
403
+
404
+ def user_config
405
+ @user_config ||= UserConfig.new(self)
406
+ end
407
+
408
+ def on_heroku?
409
+ File.exist?(get(:'heroku.dyno_info_path'))
410
+ end
411
+
412
+ private
413
+
414
+ def create_logger(out)
415
+ if out.is_a?(String)
416
+ out = File.expand_path(out, root)
417
+ # May be redundant since we also do this in the permissions check
418
+ FileUtils.mkdir_p(File.dirname(out))
419
+ end
420
+
421
+ Logger.new(out)
422
+ rescue
423
+ Logger.new(STDOUT)
424
+ end
425
+
426
+ def load_logger
427
+ unless l = @logger
428
+ out = get(:log_file)
429
+ out = STDOUT if out == '-'
430
+
431
+ l = create_logger(out)
432
+ l.level =
433
+ case get(:log_level)
434
+ when /^debug$/i then Logger::DEBUG
435
+ when /^info$/i then Logger::INFO
436
+ when /^warn$/i then Logger::WARN
437
+ when /^error$/i then Logger::ERROR
438
+ end
439
+ end
440
+
441
+ l
442
+ end
443
+
444
+ def cast_for_env(v)
445
+ case v
446
+ when true then 'true'
447
+ when false then 'false'
448
+ when nil then 'nil'
449
+ else v.to_s
450
+ end
451
+ end
452
+
453
+ end
454
+ end
@@ -0,0 +1,6 @@
1
+ module Skylight
2
+ module Core
3
+ # @api private
4
+ class ConfigError < RuntimeError; end
5
+ end
6
+ end
@@ -0,0 +1,44 @@
1
+ module Skylight
2
+ module Core
3
+ module Fanout
4
+
5
+ def self.registered
6
+ @registered ||= []
7
+ end
8
+
9
+ def self.register(obj)
10
+ registered.push(obj)
11
+ end
12
+
13
+ def self.unregister(obj)
14
+ registered.delete(obj)
15
+ end
16
+
17
+ def self.trace(*args)
18
+ registered.map{|r| r.trace(*args) }
19
+ end
20
+
21
+ def self.instrument(*args)
22
+ if block_given?
23
+ spans = instrument(*args)
24
+ begin
25
+ yield spans
26
+ ensure
27
+ done(spans)
28
+ end
29
+ else
30
+ registered.map do |r|
31
+ [r, r.instrument(*args)]
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.done(spans, meta=nil)
37
+ spans.reverse.each do |(target, span)|
38
+ target.done(span, meta)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ module Skylight
2
+ module Core
3
+ module Formatters
4
+ module HTTP
5
+
6
+ # Build instrumentation options for HTTP queries
7
+ #
8
+ # @param [String] method HTTP method, e.g. get, post
9
+ # @param [String] scheme HTTP scheme, e.g. http, https
10
+ # @param [String] host Request host, e.g. example.com
11
+ # @param [String, Integer] port Request port
12
+ # @param [String] path Request path
13
+ # @param [String] query Request query string
14
+ # @return [Hash] a hash containing `:category`, `:title`, and `:annotations`
15
+ def self.build_opts(method, scheme, host, port, path, query)
16
+ { category: "api.http.#{method.downcase}",
17
+ title: "#{method.upcase} #{host}",
18
+ meta: { host: host } }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,107 @@
1
+ require 'thread'
2
+
3
+ module Skylight::Core
4
+ # @api private
5
+ class GC
6
+ METHODS = [ :enable, :total_time ]
7
+ TH_KEY = :SK_GC_CURR_WINDOW
8
+ MAX_COUNT = 1000
9
+ MAX_TIME = 30_000_000
10
+
11
+ include Util::Logging
12
+
13
+ attr_reader :config
14
+
15
+ def initialize(config, profiler)
16
+ @listeners = []
17
+ @config = config
18
+ @lock = Mutex.new
19
+ @time = 0
20
+
21
+ if METHODS.all? { |m| profiler.respond_to?(m) }
22
+ @profiler = profiler
23
+ @time = @profiler.total_time
24
+ else
25
+ debug "disabling GC profiling"
26
+ end
27
+ end
28
+
29
+ def enable
30
+ @profiler.enable if @profiler
31
+ end
32
+
33
+ def track
34
+ unless @profiler
35
+ win = Window.new(nil)
36
+ else
37
+ win = Window.new(self)
38
+
39
+ @lock.synchronize do
40
+ __update
41
+ @listeners << win
42
+
43
+ # Cleanup any listeners that might have leaked
44
+ until @listeners[0].time < MAX_TIME
45
+ @listeners.shift
46
+ end
47
+
48
+ if @listeners.length > MAX_COUNT
49
+ @listeners.shift
50
+ end
51
+ end
52
+ end
53
+
54
+ win
55
+ end
56
+
57
+ def release(win)
58
+ @lock.synchronize do
59
+ @listeners.delete(win)
60
+ end
61
+ end
62
+
63
+ def update
64
+ @lock.synchronize do
65
+ __update
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ private
72
+
73
+ def __update
74
+ time = @profiler.total_time
75
+ diff = time - @time
76
+ @time = time
77
+
78
+ if diff > 0
79
+ @listeners.each do |l|
80
+ l.add(diff)
81
+ end
82
+ end
83
+ end
84
+
85
+ class Window
86
+ attr_reader :time
87
+
88
+ def initialize(global)
89
+ @global = global
90
+ @time = 0
91
+ end
92
+
93
+ def update
94
+ @global.update if @global
95
+ end
96
+
97
+ def add(time)
98
+ @time += time
99
+ end
100
+
101
+ def release
102
+ @global.release(self) if @global
103
+ end
104
+ end
105
+
106
+ end
107
+ end