zookeeper 0.9.3-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/CHANGELOG +119 -0
- data/Gemfile +17 -0
- data/LICENSE +23 -0
- data/Manifest +29 -0
- data/README.markdown +59 -0
- data/Rakefile +139 -0
- data/examples/cloud_config.rb +125 -0
- data/ext/.gitignore +6 -0
- data/ext/Rakefile +51 -0
- data/ext/c_zookeeper.rb +212 -0
- data/ext/dbg.h +53 -0
- data/ext/depend +5 -0
- data/ext/extconf.rb +85 -0
- data/ext/generate_gvl_code.rb +316 -0
- data/ext/zkc-3.3.5.tar.gz +0 -0
- data/ext/zkrb_wrapper.c +731 -0
- data/ext/zkrb_wrapper.h +330 -0
- data/ext/zkrb_wrapper_compat.c +15 -0
- data/ext/zkrb_wrapper_compat.h +11 -0
- data/ext/zookeeper_base.rb +211 -0
- data/ext/zookeeper_c.c +725 -0
- data/ext/zookeeper_lib.c +677 -0
- data/ext/zookeeper_lib.h +172 -0
- data/java/zookeeper_base.rb +477 -0
- data/lib/zookeeper.rb +297 -0
- data/lib/zookeeper/acls.rb +40 -0
- data/lib/zookeeper/callbacks.rb +91 -0
- data/lib/zookeeper/common.rb +174 -0
- data/lib/zookeeper/common/queue_with_pipe.rb +78 -0
- data/lib/zookeeper/constants.rb +57 -0
- data/lib/zookeeper/em_client.rb +55 -0
- data/lib/zookeeper/exceptions.rb +100 -0
- data/lib/zookeeper/stat.rb +21 -0
- data/lib/zookeeper/version.rb +6 -0
- data/notes.txt +14 -0
- data/spec/c_zookeeper_spec.rb +50 -0
- data/spec/chrooted_connection_spec.rb +81 -0
- data/spec/default_watcher_spec.rb +41 -0
- data/spec/em_spec.rb +51 -0
- data/spec/log4j.properties +17 -0
- data/spec/shared/all_success_return_values.rb +10 -0
- data/spec/shared/connection_examples.rb +1018 -0
- data/spec/spec_helper.rb +119 -0
- data/spec/support/progress_formatter.rb +15 -0
- data/spec/zookeeper_spec.rb +24 -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 +181 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module ZookeeperCommon
|
2
|
+
# Ceci n'est pas une pipe
|
3
|
+
class QueueWithPipe
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :@queue, :clear
|
7
|
+
|
8
|
+
# raised when close has been called, and pop() is performed
|
9
|
+
#
|
10
|
+
class ShutdownException < StandardError; end
|
11
|
+
|
12
|
+
# @private
|
13
|
+
KILL_TOKEN = Object.new unless defined?(KILL_TOKEN)
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
# r, w = IO.pipe
|
17
|
+
# @pipe = { :read => r, :write => w }
|
18
|
+
@queue = Queue.new
|
19
|
+
|
20
|
+
# with the EventMachine client, we want to let EM handle clearing the
|
21
|
+
# event pipe, so we set this to false
|
22
|
+
# @clear_reads_on_pop = true
|
23
|
+
|
24
|
+
@mutex = Mutex.new
|
25
|
+
@closed = false
|
26
|
+
@graceful = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def push(obj)
|
30
|
+
logger.debug { "#{self.class}##{__method__} obj: #{obj.inspect}, kill_token? #{obj == KILL_TOKEN}" }
|
31
|
+
@queue.push(obj)
|
32
|
+
end
|
33
|
+
|
34
|
+
def pop(non_blocking=false)
|
35
|
+
raise ShutdownException if closed? # this may get us in trouble
|
36
|
+
|
37
|
+
rv = @queue.pop(non_blocking)
|
38
|
+
|
39
|
+
if rv == KILL_TOKEN
|
40
|
+
close
|
41
|
+
raise ShutdownException
|
42
|
+
end
|
43
|
+
|
44
|
+
rv
|
45
|
+
end
|
46
|
+
|
47
|
+
# close the queue and causes ShutdownException to be raised on waiting threads
|
48
|
+
def graceful_close!
|
49
|
+
@mutex.synchronize do
|
50
|
+
return if @graceful or @closed
|
51
|
+
logger.debug { "#{self.class}##{__method__} gracefully closing" }
|
52
|
+
@graceful = true
|
53
|
+
push(KILL_TOKEN)
|
54
|
+
end
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def close
|
59
|
+
@mutex.synchronize do
|
60
|
+
return if @closed
|
61
|
+
@closed = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def closed?
|
66
|
+
@mutex.synchronize { !!@closed }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def clear_reads_on_pop?
|
71
|
+
@clear_reads_on_pop
|
72
|
+
end
|
73
|
+
|
74
|
+
def logger
|
75
|
+
Zookeeper.logger
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ZookeeperConstants
|
2
|
+
# file type masks
|
3
|
+
ZOO_EPHEMERAL = 1
|
4
|
+
ZOO_SEQUENCE = 2
|
5
|
+
|
6
|
+
# session state
|
7
|
+
ZOO_EXPIRED_SESSION_STATE = -112
|
8
|
+
ZOO_AUTH_FAILED_STATE = -113
|
9
|
+
ZOO_CLOSED_STATE = 0
|
10
|
+
ZOO_CONNECTING_STATE = 1
|
11
|
+
ZOO_ASSOCIATING_STATE = 2
|
12
|
+
ZOO_CONNECTED_STATE = 3
|
13
|
+
|
14
|
+
# watch types
|
15
|
+
ZOO_CREATED_EVENT = 1
|
16
|
+
ZOO_DELETED_EVENT = 2
|
17
|
+
ZOO_CHANGED_EVENT = 3
|
18
|
+
ZOO_CHILD_EVENT = 4
|
19
|
+
ZOO_SESSION_EVENT = -1
|
20
|
+
ZOO_NOTWATCHING_EVENT = -2
|
21
|
+
|
22
|
+
# only used by the C extension
|
23
|
+
ZOO_LOG_LEVEL_ERROR = 1
|
24
|
+
ZOO_LOG_LEVEL_WARN = 2
|
25
|
+
ZOO_LOG_LEVEL_INFO = 3
|
26
|
+
ZOO_LOG_LEVEL_DEBUG = 4
|
27
|
+
|
28
|
+
# used to find the name for a numeric event
|
29
|
+
# @private
|
30
|
+
EVENT_TYPE_NAMES = {
|
31
|
+
1 => 'created',
|
32
|
+
2 => 'deleted',
|
33
|
+
3 => 'changed',
|
34
|
+
4 => 'child',
|
35
|
+
-1 => 'session',
|
36
|
+
-2 => 'notwatching',
|
37
|
+
}
|
38
|
+
|
39
|
+
# used to pretty print the state name
|
40
|
+
# @private
|
41
|
+
STATE_NAMES = {
|
42
|
+
-112 => 'expired_session',
|
43
|
+
-113 => 'auth_failed',
|
44
|
+
0 => 'closed',
|
45
|
+
1 => 'connecting',
|
46
|
+
2 => 'associating',
|
47
|
+
3 => 'connected',
|
48
|
+
}
|
49
|
+
|
50
|
+
def event_by_value(v)
|
51
|
+
(name = EVENT_TYPE_NAMES[v]) ? "ZOO_#{name.upcase}_EVENT" : ''
|
52
|
+
end
|
53
|
+
|
54
|
+
def state_by_value(v)
|
55
|
+
(name = STATE_NAMES[v]) ? "ZOO_#{name.upcase}_STATE" : ''
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'zookeeper'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
module ZookeeperEM
|
5
|
+
class Client < Zookeeper
|
6
|
+
# @private
|
7
|
+
# the EM Connection instance we receive once we call EM.watch on our selectable_io
|
8
|
+
attr_reader :em_connection
|
9
|
+
|
10
|
+
def initialize(*a, &b)
|
11
|
+
@on_close = EM::DefaultDeferrable.new
|
12
|
+
@on_attached = EM::DefaultDeferrable.new
|
13
|
+
@em_connection = nil
|
14
|
+
logger.debug { "ZookeeperEM::Client obj_id %x: init" % [object_id] }
|
15
|
+
super(*a, &b)
|
16
|
+
on_attached.succeed
|
17
|
+
end
|
18
|
+
|
19
|
+
# EM::DefaultDeferrable that will be called back when our em_connection has been detached
|
20
|
+
# and we've completed the close operation
|
21
|
+
def on_close(&block)
|
22
|
+
@on_close.callback(&block) if block
|
23
|
+
@on_close
|
24
|
+
end
|
25
|
+
|
26
|
+
# called after we've successfully registered our selectable_io to be
|
27
|
+
# managed by the EM reactor
|
28
|
+
def on_attached(&block)
|
29
|
+
@on_attached.callback(&block) if block
|
30
|
+
@on_attached
|
31
|
+
end
|
32
|
+
|
33
|
+
def dispatch_next_callback(hash)
|
34
|
+
EM.schedule do
|
35
|
+
if running? and not closed?
|
36
|
+
logger.debug { "#{self.class}##{__method__} dispatch_next_callback: #{hash.inspect}: reactor_thread? #{EM.reactor_thread?}, running? #{running?}, closed? #{closed?}" }
|
37
|
+
super(hash)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def close(&block)
|
43
|
+
on_close(&block)
|
44
|
+
super()
|
45
|
+
on_close.succeed
|
46
|
+
end
|
47
|
+
|
48
|
+
# Because eventmachine is single-threaded, and events are dispatched on the
|
49
|
+
# reactor thread we just delegate this to EM.reactor_thread?
|
50
|
+
def event_dispatch_thread?
|
51
|
+
EM.reactor_thread?
|
52
|
+
end
|
53
|
+
end # Client
|
54
|
+
end # ZookeeperEM
|
55
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module ZookeeperExceptions
|
2
|
+
# exceptions/errors
|
3
|
+
ZOK = 0
|
4
|
+
ZSYSTEMERROR = -1
|
5
|
+
ZRUNTIMEINCONSISTENCY = -2
|
6
|
+
ZDATAINCONSISTENCY = -3
|
7
|
+
ZCONNECTIONLOSS = -4
|
8
|
+
ZMARSHALLINGERROR = -5
|
9
|
+
ZUNIMPLEMENTED = -6
|
10
|
+
ZOPERATIONTIMEOUT = -7
|
11
|
+
ZBADARGUMENTS = -8
|
12
|
+
ZINVALIDSTATE = -9
|
13
|
+
|
14
|
+
# api errors
|
15
|
+
ZAPIERROR = -100
|
16
|
+
ZNONODE = -101
|
17
|
+
ZNOAUTH = -102
|
18
|
+
ZBADVERSION = -103
|
19
|
+
ZNOCHILDRENFOREPHEMERALS = -108
|
20
|
+
ZNODEEXISTS = -110
|
21
|
+
ZNOTEMPTY = -111
|
22
|
+
ZSESSIONEXPIRED = -112
|
23
|
+
ZINVALIDCALLBACK = -113
|
24
|
+
ZINVALIDACL = -114
|
25
|
+
ZAUTHFAILED = -115
|
26
|
+
ZCLOSING = -116
|
27
|
+
ZNOTHING = -117
|
28
|
+
ZSESSIONMOVED = -118
|
29
|
+
|
30
|
+
class ZookeeperException < StandardError
|
31
|
+
class EverythingOk < ZookeeperException; end
|
32
|
+
class SystemError < ZookeeperException; end
|
33
|
+
class RunTimeInconsistency < ZookeeperException; end
|
34
|
+
class DataInconsistency < ZookeeperException; end
|
35
|
+
class ConnectionLoss < ZookeeperException; end
|
36
|
+
class MarshallingError < ZookeeperException; end
|
37
|
+
class Unimplemented < ZookeeperException; end
|
38
|
+
class OperationTimeOut < ZookeeperException; end
|
39
|
+
class BadArguments < ZookeeperException; end
|
40
|
+
class InvalidState < ZookeeperException; end
|
41
|
+
class ApiError < ZookeeperException; end
|
42
|
+
class NoNode < ZookeeperException; end
|
43
|
+
class NoAuth < ZookeeperException; end
|
44
|
+
class BadVersion < ZookeeperException; end
|
45
|
+
class NoChildrenForEphemerals < ZookeeperException; end
|
46
|
+
class NodeExists < ZookeeperException; end
|
47
|
+
class NotEmpty < ZookeeperException; end
|
48
|
+
class SessionExpired < ZookeeperException; end
|
49
|
+
class InvalidCallback < ZookeeperException; end
|
50
|
+
class InvalidACL < ZookeeperException; end
|
51
|
+
class AuthFailed < ZookeeperException; end
|
52
|
+
class Closing < ZookeeperException; end
|
53
|
+
class Nothing < ZookeeperException; end
|
54
|
+
class SessionMoved < ZookeeperException; end
|
55
|
+
|
56
|
+
# these are Ruby client exceptions
|
57
|
+
class ConnectionClosed < ZookeeperException; end
|
58
|
+
class NotConnected < ZookeeperException; end
|
59
|
+
class ShuttingDownException < ZookeeperException; end
|
60
|
+
class DataTooLargeException < ZookeeperException; end
|
61
|
+
|
62
|
+
# yes, make an alias, this is the way zookeeper refers to it
|
63
|
+
ExpiredSession = SessionExpired
|
64
|
+
|
65
|
+
def self.by_code(code)
|
66
|
+
case code
|
67
|
+
when ZOK then EverythingOk
|
68
|
+
when ZSYSTEMERROR then SystemError
|
69
|
+
when ZRUNTIMEINCONSISTENCY then RunTimeInconsistency
|
70
|
+
when ZDATAINCONSISTENCY then DataInconsistency
|
71
|
+
when ZCONNECTIONLOSS then ConnectionLoss
|
72
|
+
when ZMARSHALLINGERROR then MarshallingError
|
73
|
+
when ZUNIMPLEMENTED then Unimplemented
|
74
|
+
when ZOPERATIONTIMEOUT then OperationTimeOut
|
75
|
+
when ZBADARGUMENTS then BadArguments
|
76
|
+
when ZINVALIDSTATE then InvalidState
|
77
|
+
when ZAPIERROR then ApiError
|
78
|
+
when ZNONODE then NoNode
|
79
|
+
when ZNOAUTH then NoAuth
|
80
|
+
when ZBADVERSION then BadVersion
|
81
|
+
when ZNOCHILDRENFOREPHEMERALS then NoChildrenForEphemerals
|
82
|
+
when ZNODEEXISTS then NodeExists
|
83
|
+
when ZNOTEMPTY then NotEmpty
|
84
|
+
when ZSESSIONEXPIRED then SessionExpired
|
85
|
+
when ZINVALIDCALLBACK then InvalidCallback
|
86
|
+
when ZINVALIDACL then InvalidACL
|
87
|
+
when ZAUTHFAILED then AuthFailed
|
88
|
+
when ZCLOSING then Closing
|
89
|
+
when ZNOTHING then Nothing
|
90
|
+
when ZSESSIONMOVED then SessionMoved
|
91
|
+
else Exception.new("no exception defined for code #{code}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.raise_on_error(code)
|
96
|
+
exc = self.by_code(code)
|
97
|
+
raise exc unless exc == EverythingOk
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ZookeeperStat
|
2
|
+
class Stat
|
3
|
+
attr_reader :version, :exists, :czxid, :mzxid, :ctime, :mtime, :cversion, :aversion, :ephemeralOwner, :dataLength, :numChildren, :pzxid
|
4
|
+
|
5
|
+
alias :ephemeral_owner :ephemeralOwner
|
6
|
+
alias :num_children :numChildren
|
7
|
+
alias :data_length :dataLength
|
8
|
+
|
9
|
+
def initialize(val)
|
10
|
+
@exists = !!val
|
11
|
+
@czxid, @mzxid, @ctime, @mtime, @version, @cversion, @aversion,
|
12
|
+
@ephemeralOwner, @dataLength, @numChildren, @pzxid = val if val.is_a?(Array)
|
13
|
+
val.each { |k,v| instance_variable_set "@#{k}", v } if val.is_a?(Hash)
|
14
|
+
raise ArgumentError unless (val.is_a?(Hash) or val.is_a?(Array) or val.nil?)
|
15
|
+
end
|
16
|
+
|
17
|
+
def exists?
|
18
|
+
@exists
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/notes.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Notes on this fork of http://github.com/twitter/zookeeper
|
2
|
+
|
3
|
+
The main purpose of this fork is to provide JRuby compatibility with the MRI driver of the original. We have been using an early fork with an incomplete binding to the C client, and had recently bumped up against a fairly serious bug with handling events. The twitter/zookeeper driver solved this problem but was lacking JRuby support (which is a core piece of our environment).
|
4
|
+
|
5
|
+
I've packaged the Java client (and its dependency on log4j) as gems that are installed separately from this codebase to cut down on the size of the zookeeper gem. If this poses a problem (for instance, if the original goal was to have an all-in-one install with no external dependencies), it would be trivial to package those jars along with this code.
|
6
|
+
|
7
|
+
In the course of writing the wrapper for JRuby, I've written nearly-complete specs for all of the public API methods: get, set, get_children, stat, create, delete, get_acl, and set_acl. The reason I say they're "nearly" complete is that I have no use for set_acl, and quite honestly, couldn't figure out how to make it work.
|
8
|
+
|
9
|
+
I'm planning on writing a companion gem, 'ZK', that would take some of the higher-level constructs that we'd written around the lower-level driver to implement features like locking and queues, and also to provide a more ruby-like interface.
|
10
|
+
|
11
|
+
I'd like to reorganize this codebase in a number of ways, most of all to move all of the various classes and modules under a common "Zookeeper" namespace, however I thought that would be a radical enough change where I'd want to discuss/coordinate with you on how exactly to do it.
|
12
|
+
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# tests the CZookeeper, obviously only available when running under MRI
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
if Module.const_defined?(:CZookeeper)
|
5
|
+
describe CZookeeper do
|
6
|
+
def pop_all_events
|
7
|
+
[].tap do |rv|
|
8
|
+
begin
|
9
|
+
rv << @event_queue.pop(non_blocking=true)
|
10
|
+
rescue ThreadError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def wait_until_connected(timeout=2)
|
16
|
+
wait_until(timeout) { @czk.state == ZookeeperConstants::ZOO_CONNECTED_STATE }
|
17
|
+
end
|
18
|
+
|
19
|
+
describe do
|
20
|
+
before do
|
21
|
+
@event_queue = ZookeeperCommon::QueueWithPipe.new
|
22
|
+
@czk = CZookeeper.new('localhost:2181', @event_queue)
|
23
|
+
end
|
24
|
+
|
25
|
+
after do
|
26
|
+
@czk.close rescue Exception
|
27
|
+
@event_queue.close rescue Exception
|
28
|
+
end
|
29
|
+
|
30
|
+
it %[should be in connected state within a reasonable amount of time] do
|
31
|
+
wait_until_connected.should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
describe :after_connected do
|
35
|
+
before do
|
36
|
+
wait_until_connected.should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it %[should have a connection event after being connected] do
|
40
|
+
event = wait_until(2) { @event_queue.pop }
|
41
|
+
event.should be
|
42
|
+
event[:req_id].should == ZookeeperCommon::ZKRB_GLOBAL_CB_REQ
|
43
|
+
event[:type].should == ZookeeperConstants::ZOO_SESSION_EVENT
|
44
|
+
event[:state].should == ZookeeperConstants::ZOO_CONNECTED_STATE
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'shared/connection_examples'
|
3
|
+
|
4
|
+
describe 'Zookeeper chrooted' do
|
5
|
+
let(:path) { "/_zkchroottest_" }
|
6
|
+
let(:data) { "underpants" }
|
7
|
+
let(:chroot_path) { '/slyphon-zookeeper-chroot' }
|
8
|
+
|
9
|
+
let(:connection_string) { "localhost:2181#{chroot_path}" }
|
10
|
+
|
11
|
+
before do
|
12
|
+
@zk = Zookeeper.new(connection_string)
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
@zk and @zk.close
|
17
|
+
end
|
18
|
+
|
19
|
+
def zk
|
20
|
+
@zk
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'non-existent' do
|
24
|
+
describe 'with existing parent' do
|
25
|
+
let(:chroot_path) { '/one-level' }
|
26
|
+
|
27
|
+
describe 'create' do
|
28
|
+
before do
|
29
|
+
with_open_zk do |z|
|
30
|
+
rm_rf(z, chroot_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it %[should successfully create the path] do
|
35
|
+
rv = zk.create(:path => '/', :data => '')
|
36
|
+
rv[:rc].should be_zero
|
37
|
+
rv[:path].should == ''
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'with missing parent' do
|
43
|
+
let(:chroot_path) { '/deeply/nested/path' }
|
44
|
+
|
45
|
+
describe 'create' do
|
46
|
+
before do
|
47
|
+
with_open_zk do |z|
|
48
|
+
rm_rf(z, chroot_path)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it %[should return ZNONODE] do
|
53
|
+
rv = zk.create(:path => '/', :data => '')
|
54
|
+
rv[:rc].should_not be_zero
|
55
|
+
rv[:rc].should == ZookeeperExceptions::ZNONODE
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
describe do
|
63
|
+
before :all do
|
64
|
+
Zookeeper.logger.warn "running before :all"
|
65
|
+
|
66
|
+
with_open_zk do |z|
|
67
|
+
z.create(:path => chroot_path, :data => '')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
after :all do
|
72
|
+
with_open_zk do |z|
|
73
|
+
rm_rf(z, chroot_path)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it_should_behave_like "connection"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|