slyphon-zookeeper 0.1.0-java
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 +8 -0
- data/CHANGELOG +23 -0
- data/LICENSE +23 -0
- data/Manifest +33 -0
- data/README +42 -0
- data/Rakefile +19 -0
- data/examples/cloud_config.rb +125 -0
- data/ext/.gitignore +5 -0
- data/ext/extconf.rb +56 -0
- data/ext/zkc-3.3.2.tar.gz +0 -0
- data/ext/zookeeper_base.rb +117 -0
- data/ext/zookeeper_c.c +571 -0
- data/ext/zookeeper_lib.c +604 -0
- data/ext/zookeeper_lib.h +165 -0
- data/java/zookeeper_base.rb +426 -0
- data/lib/zookeeper.rb +180 -0
- data/lib/zookeeper/acls.rb +40 -0
- data/lib/zookeeper/callbacks.rb +89 -0
- data/lib/zookeeper/common.rb +97 -0
- data/lib/zookeeper/constants.rb +54 -0
- data/lib/zookeeper/exceptions.rb +92 -0
- data/lib/zookeeper/stat.rb +17 -0
- data/notes.txt +14 -0
- data/slyphon-zookeeper.gemspec +31 -0
- data/spec/default_watcher_spec.rb +41 -0
- data/spec/log4j.properties +17 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/zookeeper_spec.rb +924 -0
- data/test/test_basic.rb +37 -0
- data/test/test_callback1.rb +36 -0
- data/test/test_close.rb +16 -0
- data/test/test_esoteric.rb +7 -0
- data/test/test_watcher1.rb +56 -0
- data/test/test_watcher2.rb +52 -0
- metadata +178 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
v0.4.3 Fix a handful of memory-related bugs, fix SIGSEGV on master change,
|
2
|
+
reduce latency of event handling, fix compilation on OSX.
|
3
|
+
|
4
|
+
v0.4.2 Add options to Zookeeper#initialize, silence most Zookeeper logs.
|
5
|
+
|
6
|
+
v0.4.1 Upgrade to ZooKeeper 3.3.2
|
7
|
+
|
8
|
+
v0.4.0. More attr-readers (StarvingMarvin) and 1.9 compatibility (tsuraan)
|
9
|
+
|
10
|
+
v0.3.2. Handle close, closed connections and expired sessions a little more gracefully.
|
11
|
+
|
12
|
+
v0.3.1. ACL bugfix.
|
13
|
+
|
14
|
+
v0.3.0. Wickman's rewrite, breaks dependencies from myelin/emaland port.
|
15
|
+
|
16
|
+
v0.2.2. Fix compatibility with stock Leopard fat-binary Ruby.
|
17
|
+
|
18
|
+
v0.2.1. No more camelcase classname.
|
19
|
+
|
20
|
+
v0.2. Bundle C dependencies, like memcached.gem.
|
21
|
+
|
22
|
+
v0.1. First release.
|
23
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (C) 2008 Phillip Pearson
|
4
|
+
Copyright (C) 2010 Twitter, Inc.
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
'Software'), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
CHANGELOG
|
2
|
+
Gemfile
|
3
|
+
LICENSE
|
4
|
+
Manifest
|
5
|
+
README
|
6
|
+
Rakefile
|
7
|
+
Session.vim
|
8
|
+
examples/cloud_config.rb
|
9
|
+
ext/extconf.rb
|
10
|
+
ext/zkc-3.3.2.tar.gz
|
11
|
+
ext/zookeeper_base.rb
|
12
|
+
ext/zookeeper_c.c
|
13
|
+
ext/zookeeper_lib.c
|
14
|
+
ext/zookeeper_lib.h
|
15
|
+
java/zookeeper_base.rb
|
16
|
+
lib/zookeeper.rb
|
17
|
+
lib/zookeeper/acls.rb
|
18
|
+
lib/zookeeper/callbacks.rb
|
19
|
+
lib/zookeeper/common.rb
|
20
|
+
lib/zookeeper/constants.rb
|
21
|
+
lib/zookeeper/exceptions.rb
|
22
|
+
lib/zookeeper/stat.rb
|
23
|
+
spec/log4j.properties
|
24
|
+
spec/spec_helper.rb
|
25
|
+
spec/zookeeper_spec.rb
|
26
|
+
tags
|
27
|
+
test/test_basic.rb
|
28
|
+
test/test_callback1.rb
|
29
|
+
test/test_close.rb
|
30
|
+
test/test_esoteric.rb
|
31
|
+
test/test_watcher1.rb
|
32
|
+
test/test_watcher2.rb
|
33
|
+
zookeeper.gemspec
|
data/README
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
zookeeper
|
2
|
+
|
3
|
+
An interface to the Zookeeper distributed configuration server.
|
4
|
+
|
5
|
+
== License
|
6
|
+
|
7
|
+
Copyright 2008 Phillip Pearson, and 2010 Twitter, Inc. Licensed under the
|
8
|
+
MIT License. See the included LICENSE file. Portions copyright 2008-2010
|
9
|
+
the Apache Software Foundation, licensed under the Apache 2 license, and
|
10
|
+
used with permission.
|
11
|
+
|
12
|
+
== Install
|
13
|
+
|
14
|
+
sudo gem install zookeeper
|
15
|
+
|
16
|
+
== Usage
|
17
|
+
|
18
|
+
Connect to a server:
|
19
|
+
|
20
|
+
require 'rubygems'
|
21
|
+
require 'zookeeper'
|
22
|
+
z = Zookeeper.new("localhost:2181")
|
23
|
+
z.get_children(:path => "/")
|
24
|
+
|
25
|
+
== Idioms
|
26
|
+
|
27
|
+
The following methods are initially supported:
|
28
|
+
get
|
29
|
+
set
|
30
|
+
get_children
|
31
|
+
stat
|
32
|
+
create
|
33
|
+
delete
|
34
|
+
get_acl
|
35
|
+
set_acl
|
36
|
+
|
37
|
+
All support async callbacks. get, get_children and stat support both
|
38
|
+
watchers and callbacks.
|
39
|
+
|
40
|
+
Calls take a dictionary of parameters. With the exception of set_acl, the
|
41
|
+
only required parameter is :path. Each call returns a dictionary with at
|
42
|
+
minimum two keys :req_id and :rc.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'echoe'
|
2
|
+
|
3
|
+
Echoe.new("zookeeper") do |p|
|
4
|
+
p.author = "Phillip Pearson, Eric Maland, Evan Weaver, Brian Wickman"
|
5
|
+
p.project = "fauna"
|
6
|
+
p.summary = "An interface to the Zookeeper distributed configuration server."
|
7
|
+
p.url = "https://github.com/twitter/zookeeper"
|
8
|
+
p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
|
9
|
+
p.clean_pattern += ["ext/lib", "ext/include", "ext/c", "ext/bin", "ext/conftest.dSYM"]
|
10
|
+
p.rdoc_pattern = /README|TODO|LICENSE|CHANGELOG|BENCH|COMPAT|zookeeper_c.c|zookeeper.rb/
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :mb do
|
14
|
+
task :build_gems do
|
15
|
+
sh "gem build slyphon-zookeeper.gemspec"
|
16
|
+
ENV['JAVA_GEM'] = '1'
|
17
|
+
sh "gem build slyphon-zookeeper.gemspec"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "zookeeper"
|
3
|
+
|
4
|
+
# A basic cloud-based YAML config library. Ruby Zookeeper client example.
|
5
|
+
#
|
6
|
+
# If you pass in a file as 'zk:/foo.yml/blah' it will go out to zookeeper.
|
7
|
+
# Otherwise the file is assumed to be local. The yml file will get parsed
|
8
|
+
# and cached locally, and keys after the .yml get interpreted as keys into
|
9
|
+
# the YAML.
|
10
|
+
#
|
11
|
+
# e.g. get(zk:/config/service.yml/key1/key2/key3..) =>
|
12
|
+
# zk.get(:path => /config/service.yml)
|
13
|
+
# yaml <= YAML.parse(data)
|
14
|
+
# yaml[key1][key2][key3]...
|
15
|
+
#
|
16
|
+
# If keys are unspecified, it returns the parsed YAML as one big object
|
17
|
+
#
|
18
|
+
# TODO if staleness is set to 0, read in YAML immediately before next
|
19
|
+
# get(...)
|
20
|
+
|
21
|
+
class CloudConfig
|
22
|
+
class NodeNotFound < StandardError; end
|
23
|
+
class BadPathError < StandardError; end
|
24
|
+
|
25
|
+
DEFAULT_SERVERS = "localhost:2181"
|
26
|
+
|
27
|
+
def initialize(zkservers = DEFAULT_SERVERS, staleness = 15) # maximum allowed staleness in seconds
|
28
|
+
@staleness = staleness
|
29
|
+
@lock = Mutex.new
|
30
|
+
@zkservers = DEFAULT_SERVERS
|
31
|
+
|
32
|
+
# cache
|
33
|
+
@data = {}
|
34
|
+
@zkcb = Zookeeper::WatcherCallback.new { dirty_callback(@zkcb.context) }
|
35
|
+
@zk = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def get(node)
|
39
|
+
filename, keys = extract_filename(node)
|
40
|
+
|
41
|
+
# read(filename) is potentially a zk call, so do not hold the lock during the read
|
42
|
+
if @lock.synchronize { !@data.has_key?(filename) }
|
43
|
+
d = YAML.load(read(filename))
|
44
|
+
@lock.synchronize { @data[filename] = d }
|
45
|
+
end
|
46
|
+
|
47
|
+
# synchronized b/c we potentially have a background thread updating data nodes from zk
|
48
|
+
# if keys is empty, return the whole file, otherwise roll up the keys
|
49
|
+
@lock.synchronize {
|
50
|
+
keys.empty? ? @data[filename] : keys.inject(@data[filename]) { |hash, key| hash[key] }
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# todo:
|
55
|
+
# factor get-and-watch into a different subsystem (so you can have
|
56
|
+
# polling stat() ops on local filesystem.)
|
57
|
+
def read(yaml)
|
58
|
+
# read yaml file and register watcher. if watcher fires, set up
|
59
|
+
# background thread to do read and update data.
|
60
|
+
if yaml.match(/^zk:/)
|
61
|
+
@zk ||= init_zk
|
62
|
+
yaml = yaml['zk:'.length..-1] # strip off zk: from zk:/config/path.yml
|
63
|
+
resp = get_and_register(yaml)
|
64
|
+
|
65
|
+
if resp[:rc] != Zookeeper::ZOK
|
66
|
+
@zk.unregister_watcher(resp[:req_id])
|
67
|
+
raise NodeNotFound
|
68
|
+
end
|
69
|
+
|
70
|
+
resp[:data]
|
71
|
+
else
|
72
|
+
raise NodeNotFound unless File.exists?(yaml)
|
73
|
+
File.read(yaml)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_filename(node)
|
78
|
+
path_elements = node.split("/")
|
79
|
+
|
80
|
+
yamlindex = path_elements.map{ |x| x.match("\.yml$") != nil }.index(true)
|
81
|
+
raise BadPathError unless yamlindex
|
82
|
+
|
83
|
+
yamlname = path_elements[0..yamlindex].join '/'
|
84
|
+
yamlkeys = path_elements[(yamlindex+1)..-1]
|
85
|
+
|
86
|
+
return yamlname, yamlkeys
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def init_zk
|
91
|
+
Zookeeper.new(@zkservers)
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_and_register(znode)
|
95
|
+
@zk.get(:path => znode, :watcher => @zkcb,
|
96
|
+
:watcher_context => { :path => znode,
|
97
|
+
:wait => rand(@staleness) })
|
98
|
+
end
|
99
|
+
|
100
|
+
def dirty_callback(context)
|
101
|
+
path = context[:path]
|
102
|
+
wait = context[:wait]
|
103
|
+
|
104
|
+
# Fire off a background update that waits a randomized period of time up
|
105
|
+
# to @staleness seconds.
|
106
|
+
Thread.new do
|
107
|
+
sleep wait
|
108
|
+
background_update(path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def background_update(zkpath)
|
113
|
+
# do a synchronous get/register a new watcher
|
114
|
+
resp = get_and_register(zkpath)
|
115
|
+
if resp[:rc] != Zookeeper::ZOK
|
116
|
+
# puts "Unable to read #{zkpath} from Zookeeper!" @logger.error
|
117
|
+
zk.unregister_watcher(resp[:req_id])
|
118
|
+
else
|
119
|
+
# puts "Updating data."
|
120
|
+
d = YAML.load(resp[:data])
|
121
|
+
@lock.synchronize { @data["zk:#{zkpath}"] = d }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
data/ext/extconf.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
require 'mkmf'
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
HERE = File.expand_path(File.dirname(__FILE__))
|
6
|
+
BUNDLE = Dir.glob("zkc-*.tar.gz").first
|
7
|
+
BUNDLE_PATH = "c"
|
8
|
+
|
9
|
+
$CFLAGS = "#{RbConfig::CONFIG['CFLAGS']} #{$CFLAGS}".gsub("$(cflags)", "").gsub("-arch ppc", "")
|
10
|
+
$LDFLAGS = "#{RbConfig::CONFIG['LDFLAGS']} #{$LDFLAGS}".gsub("$(ldflags)", "").gsub("-arch ppc", "")
|
11
|
+
$CXXFLAGS = " -std=gnu++98 #{$CFLAGS}"
|
12
|
+
$CPPFLAGS = $ARCH_FLAG = $DLDFLAGS = ""
|
13
|
+
|
14
|
+
if ENV['DEBUG']
|
15
|
+
puts "Setting debug flags."
|
16
|
+
$CFLAGS << " -O0 -ggdb3 -DHAVE_DEBUG"
|
17
|
+
$EXTRA_CONF = " --enable-debug"
|
18
|
+
$CFLAGS.gsub!(/ -O[^0] /, ' ')
|
19
|
+
end
|
20
|
+
|
21
|
+
$includes = " -I#{HERE}/include"
|
22
|
+
$libraries = " -L#{HERE}/lib -L#{RbConfig::CONFIG['libdir']}"
|
23
|
+
$CFLAGS = "#{$includes} #{$libraries} #{$CFLAGS}"
|
24
|
+
$LDFLAGS = "#{$libraries} #{$LDFLAGS}"
|
25
|
+
$LIBPATH = ["#{HERE}/lib"]
|
26
|
+
$DEFLIBPATH = []
|
27
|
+
|
28
|
+
Dir.chdir(HERE) do
|
29
|
+
if File.exist?("lib")
|
30
|
+
puts "Zkc already built; run 'rake clean' first if you need to rebuild."
|
31
|
+
else
|
32
|
+
puts "Building zkc."
|
33
|
+
puts(cmd = "tar xzf #{BUNDLE} 2>&1")
|
34
|
+
raise "'#{cmd}' failed" unless system(cmd)
|
35
|
+
|
36
|
+
Dir.chdir(BUNDLE_PATH) do
|
37
|
+
puts(cmd = "env CC=gcc CXX=g++ CFLAGS='-fPIC #{$CFLAGS}' LDFLAGS='-fPIC #{$LDFLAGS}' ./configure --prefix=#{HERE} --without-cppunit --disable-dependency-tracking #{$EXTRA_CONF} 2>&1")
|
38
|
+
raise "'#{cmd}' failed" unless system(cmd)
|
39
|
+
puts(cmd = "make CXXFLAGS='#{$CXXFLAGS}' CFLAGS='-fPIC #{$CFLAGS}' LDFLAGS='-fPIC #{$LDFLAGS}' || true 2>&1")
|
40
|
+
raise "'#{cmd}' failed" unless system(cmd)
|
41
|
+
puts(cmd = "make install || true 2>&1")
|
42
|
+
raise "'#{cmd}' failed" unless system(cmd)
|
43
|
+
end
|
44
|
+
|
45
|
+
system("rm -rf #{BUNDLE_PATH}") unless ENV['DEBUG'] or ENV['DEV']
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Absolutely prevent the linker from picking up any other zookeeper_mt
|
50
|
+
Dir.chdir("#{HERE}/lib") do
|
51
|
+
system("cp -f libzookeeper_mt.a libzookeeper_mt_gem.a")
|
52
|
+
system("cp -f libzookeeper_mt.la libzookeeper_mt_gem.la")
|
53
|
+
end
|
54
|
+
$LIBS << " -lzookeeper_mt_gem"
|
55
|
+
|
56
|
+
create_makefile 'zookeeper_c'
|
Binary file
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# The low-level wrapper-specific methods for the C lib
|
2
|
+
# subclassed by the top-level Zookeeper class
|
3
|
+
class ZookeeperBase < CZookeeper
|
4
|
+
include ZookeeperCommon
|
5
|
+
include ZookeeperCallbacks
|
6
|
+
include ZookeeperConstants
|
7
|
+
include ZookeeperExceptions
|
8
|
+
include ZookeeperACLs
|
9
|
+
include ZookeeperStat
|
10
|
+
|
11
|
+
|
12
|
+
ZKRB_GLOBAL_CB_REQ = -1
|
13
|
+
|
14
|
+
# debug levels
|
15
|
+
ZOO_LOG_LEVEL_ERROR = 1
|
16
|
+
ZOO_LOG_LEVEL_WARN = 2
|
17
|
+
ZOO_LOG_LEVEL_INFO = 3
|
18
|
+
ZOO_LOG_LEVEL_DEBUG = 4
|
19
|
+
|
20
|
+
def reopen(timeout = 10, watcher=nil)
|
21
|
+
watcher ||= @default_watcher
|
22
|
+
|
23
|
+
@req_mutex.synchronize do
|
24
|
+
# flushes all outstanding watcher reqs.
|
25
|
+
@watcher_req = {}
|
26
|
+
set_default_global_watcher(&watcher)
|
27
|
+
end
|
28
|
+
|
29
|
+
init(@host)
|
30
|
+
|
31
|
+
if timeout > 0
|
32
|
+
time_to_stop = Time.now + timeout
|
33
|
+
until state == Zookeeper::ZOO_CONNECTED_STATE
|
34
|
+
break if Time.now > time_to_stop
|
35
|
+
sleep 0.1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
state
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(host, timeout = 10, watcher=nil)
|
43
|
+
@watcher_reqs = {}
|
44
|
+
@completion_reqs = {}
|
45
|
+
@req_mutex = Monitor.new
|
46
|
+
@current_req_id = 1
|
47
|
+
@host = host
|
48
|
+
|
49
|
+
watcher ||= get_default_global_watcher
|
50
|
+
|
51
|
+
@_running = nil # used by the C layer
|
52
|
+
reopen(timeout, watcher)
|
53
|
+
return nil unless connected?
|
54
|
+
setup_dispatch_thread!
|
55
|
+
end
|
56
|
+
|
57
|
+
# if either of these happen, the user will need to renegotiate a connection via reopen
|
58
|
+
def assert_open
|
59
|
+
raise ZookeeperException::SessionExpired if state == ZOO_EXPIRED_SESSION_STATE
|
60
|
+
raise ZookeeperException::ConnectionClosed unless connected?
|
61
|
+
end
|
62
|
+
|
63
|
+
def connected?
|
64
|
+
state == ZOO_CONNECTED_STATE
|
65
|
+
end
|
66
|
+
|
67
|
+
def connecting?
|
68
|
+
state == ZOO_CONNECTING_STATE
|
69
|
+
end
|
70
|
+
|
71
|
+
def associating?
|
72
|
+
state == ZOO_ASSOCIATING_STATE
|
73
|
+
end
|
74
|
+
|
75
|
+
def close
|
76
|
+
@_running = false;
|
77
|
+
wake_event_loop!
|
78
|
+
|
79
|
+
@dispatcher.join
|
80
|
+
|
81
|
+
super
|
82
|
+
end
|
83
|
+
|
84
|
+
# set the watcher object/proc that will receive all global events (such as session/state events)
|
85
|
+
def set_default_global_watcher(&block)
|
86
|
+
@req_mutex.synchronize do
|
87
|
+
@default_watcher = block # save this here for reopen() to use
|
88
|
+
@watcher_reqs[ZKRB_GLOBAL_CB_REQ] = { :watcher => @default_watcher, :watcher_context => nil }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
def running?
|
94
|
+
false|@_running
|
95
|
+
end
|
96
|
+
|
97
|
+
def setup_dispatch_thread!
|
98
|
+
@dispatcher = Thread.new do
|
99
|
+
while running?
|
100
|
+
begin # calling user code, so protect ourselves
|
101
|
+
dispatch_next_callback
|
102
|
+
rescue Exception => e
|
103
|
+
$stderr.puts "Error in dispatch thread, #{e.class}: #{e.message}\n" << e.backtrace.map{|n| "\t#{n}"}.join("\n")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# TODO: Make all global puts configurable
|
110
|
+
def get_default_global_watcher
|
111
|
+
Proc.new { |args|
|
112
|
+
logger.debug { "Ruby ZK Global CB called type=#{event_by_value(args[:type])} state=#{state_by_value(args[:state])}" }
|
113
|
+
true
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|