sequel-unicache 0.9.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +144 -0
- data/Rakefile +11 -0
- data/lib/sequel/unicache.rb +18 -0
- data/lib/sequel/unicache/configuration.rb +111 -0
- data/lib/sequel/unicache/expire.rb +19 -0
- data/lib/sequel/unicache/finder.rb +52 -0
- data/lib/sequel/unicache/global_configuration.rb +79 -0
- data/lib/sequel/unicache/hook.rb +56 -0
- data/lib/sequel/unicache/logger.rb +15 -0
- data/lib/sequel/unicache/transaction.rb +20 -0
- data/lib/sequel/unicache/version.rb +5 -0
- data/lib/sequel/unicache/write.rb +122 -0
- data/sequel-unicache.gemspec +35 -0
- data/spec/configuration_spec.rb +121 -0
- data/spec/finder_spec.rb +173 -0
- data/spec/global_configuration_spec.rb +56 -0
- data/spec/log_spec.rb +28 -0
- data/spec/memcache.yml.example +8 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/write_spec.rb +277 -0
- metadata +221 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
describe Sequel::Unicache::GlobalConfiguration do
|
2
|
+
it 'should be true' do
|
3
|
+
expect(Sequel::Unicache.config).to be_kind_of Sequel::Unicache::GlobalConfiguration
|
4
|
+
end
|
5
|
+
|
6
|
+
it 'can configure for unicache' do
|
7
|
+
logger = Logger.new(STDERR)
|
8
|
+
|
9
|
+
Sequel::Unicache.configure cache: memcache,
|
10
|
+
ttl: 30,
|
11
|
+
logger: logger
|
12
|
+
|
13
|
+
expect(Sequel::Unicache.config.cache).to be memcache
|
14
|
+
expect(Sequel::Unicache.config.ttl).to be 30
|
15
|
+
expect(Sequel::Unicache.config.enabled).to be true
|
16
|
+
expect(Sequel::Unicache.config.logger).to be logger
|
17
|
+
|
18
|
+
serialize_proc = ->(model, opts) { Marshal.dump model }
|
19
|
+
deserialize_proc = ->(cache, opts) { Marshal.load cache }
|
20
|
+
key_proc = ->(hash, opts) { "id/#{hash[:id]}" }
|
21
|
+
|
22
|
+
Sequel::Unicache.config.serialize = serialize_proc
|
23
|
+
Sequel::Unicache.config.deserialize = deserialize_proc
|
24
|
+
Sequel::Unicache.config.key = key_proc
|
25
|
+
|
26
|
+
expect(Sequel::Unicache.config.serialize).to be serialize_proc
|
27
|
+
expect(Sequel::Unicache.config.deserialize).to be deserialize_proc
|
28
|
+
expect(Sequel::Unicache.config.key).to be key_proc
|
29
|
+
|
30
|
+
expect(Sequel::Unicache.config.to_h).to eq cache: memcache,
|
31
|
+
ttl: 30,
|
32
|
+
enabled: true,
|
33
|
+
logger: logger,
|
34
|
+
serialize: serialize_proc,
|
35
|
+
deserialize: deserialize_proc,
|
36
|
+
key: key_proc
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can enable & disable unicache feature' do
|
40
|
+
Sequel::Unicache.disable
|
41
|
+
expect(Sequel::Unicache.enabled?).to be false
|
42
|
+
Sequel::Unicache.enable
|
43
|
+
expect(Sequel::Unicache.enabled?).to be true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'can suspend & unsuspend read-through' do
|
47
|
+
Sequel::Unicache.suspend_unicache
|
48
|
+
expect(Sequel::Unicache.unicache_suspended?).to be true
|
49
|
+
Sequel::Unicache.unsuspend_unicache
|
50
|
+
expect(Sequel::Unicache.unicache_suspended?).to be false
|
51
|
+
Sequel::Unicache.suspend_unicache do
|
52
|
+
expect(Sequel::Unicache.unicache_suspended?).to be true
|
53
|
+
end
|
54
|
+
expect(Sequel::Unicache.unicache_suspended?).to be false
|
55
|
+
end
|
56
|
+
end
|
data/spec/log_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
describe Sequel::Unicache::Logger do
|
2
|
+
let!(:logger) { Logger.new STDOUT }
|
3
|
+
let!(:user_id) { User.first.id }
|
4
|
+
before :each do
|
5
|
+
Sequel::Unicache.configure cache: memcache, logger: logger
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'read through' do
|
9
|
+
it 'should log down and ignore exception if failed to serialize' do
|
10
|
+
User.instance_exec { unicache :id, serialize: ->(values, _) { raise 'test' } }
|
11
|
+
expect(logger).to receive(:error).at_least(:once)
|
12
|
+
user = User[user_id]
|
13
|
+
cache = memcache.get "User:id:#{user.id}"
|
14
|
+
expect(cache).to be_nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'expire' do
|
19
|
+
it 'should log down, ignore exception when failed to expire during model destroy' do
|
20
|
+
user = User[user_id]
|
21
|
+
cache = memcache.get "User:id:#{user.id}"
|
22
|
+
expect(cache).not_to be_nil
|
23
|
+
User.instance_exec { unicache :id, key: ->(values, _) { raise 'test' } }
|
24
|
+
expect(logger).to receive(:fatal).at_least(:once)
|
25
|
+
user.destroy
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
|
3
|
+
begin
|
4
|
+
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', __dir__)
|
5
|
+
Bundler.setup
|
6
|
+
rescue Bundler::GemNotFound
|
7
|
+
abort "Bundler couldn't find some gems.\n" \
|
8
|
+
'Did you run `bundle install`?'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'erb'
|
12
|
+
require 'logger'
|
13
|
+
require 'json'
|
14
|
+
require 'yaml'
|
15
|
+
|
16
|
+
require 'sequel/unicache'
|
17
|
+
require 'active_support/core_ext/hash/keys'
|
18
|
+
require 'dalli'
|
19
|
+
require 'pry'
|
20
|
+
|
21
|
+
module Helpers
|
22
|
+
def memcache
|
23
|
+
@cache ||= begin
|
24
|
+
memcache_config = YAML.load(ERB.new(File.read(File.expand_path('memcache.yml', __dir__))).result)
|
25
|
+
|
26
|
+
hosts = memcache_config['servers'].map {|server| "#{server['host']}:#{server['port']}" }
|
27
|
+
opts = memcache_config['options'] || {}
|
28
|
+
|
29
|
+
client = Dalli::Client.new hosts, opts.symbolize_keys
|
30
|
+
client.alive!
|
31
|
+
client
|
32
|
+
rescue Dalli::RingError
|
33
|
+
abort "Memcache Server is unavailable."
|
34
|
+
rescue Errno::ENOENT
|
35
|
+
abort "You must configure memcache in spec/memcache.yml before the testing.\n" \
|
36
|
+
"Copy from spec/memcache.yml.example then modify base on it will be recommended."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize_models
|
41
|
+
user_class = Class.new Sequel::Model
|
42
|
+
user_class.set_dataset database[:users]
|
43
|
+
user_class.many_to_one :manager, class: :User
|
44
|
+
Object.send :const_set, :User, user_class
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear_models
|
48
|
+
Object.send :remove_const, :User
|
49
|
+
end
|
50
|
+
|
51
|
+
def database
|
52
|
+
@database ||= begin
|
53
|
+
db = Sequel.sqlite
|
54
|
+
db.run <<-SQL
|
55
|
+
CREATE TABLE users(id INTEGER PRIMARY KEY AUTOINCREMENT, manager_id INTEGER,
|
56
|
+
username VARCHAR NOT NULL, password VARCHAR,
|
57
|
+
company_name VARCHAR NOT NULL, department VARCHAR NOT NULL,
|
58
|
+
employee_id INTEGER NOT NULL, created_at DEFAULT CURRENT_TIMESTAMP,
|
59
|
+
FOREIGN KEY(manager_id) REFERENCES users(id));
|
60
|
+
CREATE UNIQUE INDEX uniq_username ON users(username);
|
61
|
+
CREATE UNIQUE INDEX uniq_employee ON users(company_name, department, employee_id);
|
62
|
+
SQL
|
63
|
+
user_id = db[:users].insert username: 'bachue@gmail.com', password: 'bachue',
|
64
|
+
company_name: 'EMC', department: 'Mozy', employee_id: 12345
|
65
|
+
boss_id = db[:users].insert username: 'gimi@emc.com', password: 'gimi',
|
66
|
+
company_name: 'EMC', department: 'Mozy', employee_id: 10000
|
67
|
+
db[:users].where(id: user_id).update manager_id: boss_id
|
68
|
+
db
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset_database
|
73
|
+
return unless @database
|
74
|
+
@database = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def reset_global_configuration
|
78
|
+
Sequel::Unicache.instance_variable_set :@config, Sequel::Unicache::GlobalConfiguration.new
|
79
|
+
Sequel::Unicache.enable
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
RSpec.configure do |config|
|
84
|
+
config.include Helpers
|
85
|
+
|
86
|
+
config.before :each do
|
87
|
+
memcache.flush_all
|
88
|
+
initialize_models
|
89
|
+
end
|
90
|
+
|
91
|
+
config.after :each do
|
92
|
+
reset_database
|
93
|
+
clear_models
|
94
|
+
reset_global_configuration
|
95
|
+
end
|
96
|
+
end
|
data/spec/write_spec.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
describe Sequel::Unicache::Write do
|
2
|
+
let!(:user_id) { User.first.id }
|
3
|
+
|
4
|
+
before :each do
|
5
|
+
Sequel::Unicache.configure cache: memcache
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'read through' do
|
9
|
+
it 'should read through cache into memcache' do
|
10
|
+
user = User[user_id]
|
11
|
+
cache = memcache.get "User:id:#{user.id}"
|
12
|
+
expect(cache).not_to be_nil
|
13
|
+
expect(Marshal.load(cache)).to eq user.values
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should serialize model into specified format' do
|
17
|
+
User.instance_exec { unicache :id, serialize: ->(values, _) { values.to_yaml } }
|
18
|
+
user = User[user_id]
|
19
|
+
cache = memcache.get "User:id:#{user.id}"
|
20
|
+
expect(cache).not_to be_nil
|
21
|
+
expect(YAML.load(cache)).to eq user.values
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should not read through cache if unicache is not enabled for this key' do
|
25
|
+
User.instance_exec { unicache :id, enabled: false }
|
26
|
+
user = User[user_id]
|
27
|
+
cache = memcache.get "User:id:#{user.id}"
|
28
|
+
expect(cache).to be_nil
|
29
|
+
user = User.find user_id
|
30
|
+
cache = memcache.get "User:id:#{user.id}"
|
31
|
+
expect(cache).to be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should not read through cache if unicache is not enabled' do
|
35
|
+
User.instance_exec { unicache :id, enabled: true }
|
36
|
+
Sequel::Unicache.disable
|
37
|
+
user = User[user_id]
|
38
|
+
cache = memcache.get "User:id:#{user.id}"
|
39
|
+
expect(cache).to be_nil
|
40
|
+
user = User.find user_id
|
41
|
+
cache = memcache.get "User:id:#{user.id}"
|
42
|
+
expect(cache).to be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should not read through cache if read-through is suspended' do
|
46
|
+
User.instance_exec { unicache :id, enabled: true }
|
47
|
+
user = Sequel::Unicache.suspend_unicache { User[user_id] }
|
48
|
+
cache = memcache.get "User:id:#{user.id}"
|
49
|
+
expect(cache).to be_nil
|
50
|
+
user = Sequel::Unicache.suspend_unicache { User.find user_id }
|
51
|
+
cache = memcache.get "User:id:#{user.id}"
|
52
|
+
expect(cache).to be_nil
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should not read through cache if condition is not permitted' do
|
56
|
+
User.instance_exec { unicache :id, if: ->(model, _) { model.company_name != 'EMC' } }
|
57
|
+
user = User[user_id]
|
58
|
+
cache = memcache.get "User:id:#{user.id}"
|
59
|
+
expect(cache).to be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should set expiration time as you wish' do
|
63
|
+
User.instance_exec { unicache :id, ttl: 1 }
|
64
|
+
user = User[user_id]
|
65
|
+
cache = memcache.get "User:id:#{user.id}"
|
66
|
+
expect(cache).not_to be_nil
|
67
|
+
sleep 1
|
68
|
+
cache = memcache.get "User:id:#{user.id}"
|
69
|
+
expect(cache).to be_nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should not read from cache when unicache is disabled' do
|
73
|
+
User[user_id] # cache data
|
74
|
+
Sequel::Unicache.disable
|
75
|
+
User[user_id].set(company_name: 'VMware').save
|
76
|
+
user = User.find user_id
|
77
|
+
expect(user.company_name).to eq 'VMware'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should not read from cache when reload' do
|
81
|
+
user = User[user_id]
|
82
|
+
Sequel::Unicache.disable
|
83
|
+
User[user_id].set(company_name: 'VMware').save
|
84
|
+
Sequel::Unicache.enable
|
85
|
+
cache = memcache.get "User:id:#{user.id}"
|
86
|
+
expect(cache).not_to be_nil
|
87
|
+
user.reload
|
88
|
+
cache = memcache.get "User:id:#{user.id}"
|
89
|
+
expect(cache).to be_nil
|
90
|
+
expect(user.company_name).to eq 'VMware'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'expire when update' do
|
95
|
+
let!(:user) { User[user_id] }
|
96
|
+
|
97
|
+
it 'should expire cache' do
|
98
|
+
cache = memcache.get "User:id:#{user.id}"
|
99
|
+
expect(cache).not_to be_nil
|
100
|
+
user.set(company_name: 'VMware').save
|
101
|
+
cache = memcache.get "User:id:#{user.id}"
|
102
|
+
expect(cache).to be_nil
|
103
|
+
user = User[user_id]
|
104
|
+
cache = memcache.get "User:id:#{user.id}"
|
105
|
+
expect(cache).not_to be_nil
|
106
|
+
expect(Marshal.load(cache)).to eq user.values
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should not expire cache until transaction is committed' do
|
110
|
+
User.db.transaction auto_savepoint: true do
|
111
|
+
user.set(company_name: 'VMware').save
|
112
|
+
cache = memcache.get "User:id:#{user.id}"
|
113
|
+
expect(cache).not_to be_nil
|
114
|
+
end
|
115
|
+
cache = memcache.get "User:id:#{user.id}"
|
116
|
+
expect(cache).to be_nil
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should not expire cache if transaction is rollbacked' do
|
120
|
+
origin = user.values.dup
|
121
|
+
User.db.transaction rollback: :always do
|
122
|
+
user.set(company_name: 'VMware').save
|
123
|
+
end
|
124
|
+
cache = memcache.get "User:id:#{user.id}"
|
125
|
+
expect(cache).not_to be_nil
|
126
|
+
expect(Marshal.load(cache)).to eq origin
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should not expire cache even if unicache is not enabled for that key' do
|
130
|
+
User.unicache_for(:id).enabled = false
|
131
|
+
origin = user.values.dup
|
132
|
+
user.set(company_name: 'VMware').save
|
133
|
+
cache = memcache.get "User:id:#{user.id}"
|
134
|
+
expect(cache).not_to be_nil
|
135
|
+
expect(Marshal.load(cache)).to eq origin
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should not expire cache even if unicache is not enabled' do
|
139
|
+
Sequel::Unicache.disable
|
140
|
+
origin = user.values.dup
|
141
|
+
user.set(company_name: 'VMware').save
|
142
|
+
cache = memcache.get "User:id:#{user.id}"
|
143
|
+
expect(cache).not_to be_nil
|
144
|
+
expect(Marshal.load(cache)).to eq origin
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should still expire cache even if read-through is suspended' do
|
148
|
+
Sequel::Unicache.suspend_unicache { user.set(company_name: 'VMware').save }
|
149
|
+
cache = memcache.get "User:id:#{user.id}"
|
150
|
+
expect(cache).to be_nil
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should still expire all cache even if model is not completed' do
|
154
|
+
memcache.flush_all # Clear all cache first
|
155
|
+
User.instance_exec { unicache :username, key: ->(values, _) { "User/username/#{values[:username]}" } }
|
156
|
+
user = User[user_id]
|
157
|
+
cache = memcache.get "User/username/bachue@gmail.com"
|
158
|
+
expect(cache).not_to be_nil
|
159
|
+
user = User.select(:id, :company_name)[user_id]
|
160
|
+
user.set(company_name: 'VMware').save
|
161
|
+
cache = memcache.get "User/username/bachue@gmail.com"
|
162
|
+
expect(cache).to be_nil
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should expire obsolate cache if any value of the unicache key is changed' do
|
166
|
+
User.instance_exec { unicache :username }
|
167
|
+
user = User[user_id]
|
168
|
+
User.db.transaction auto_savepoint: true do
|
169
|
+
user.set(username: 'bachue@emc.com').save
|
170
|
+
user.set(username: 'bachue@vmware.com', company_name: 'VMware').save
|
171
|
+
end
|
172
|
+
cache = memcache.get "User:username:bachue@gmail.com"
|
173
|
+
expect(cache).to be_nil
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'should still get currect value during a transaction' do
|
177
|
+
user = User[user_id]
|
178
|
+
expect(Sequel::Unicache.unicache_suspended?).to be false
|
179
|
+
User.db.transaction auto_savepoint: true do
|
180
|
+
expect(Sequel::Unicache.unicache_suspended?).to be true
|
181
|
+
user.set(username: 'bachue@emc.com').save
|
182
|
+
expect(User[user_id].username).to eq 'bachue@emc.com'
|
183
|
+
end
|
184
|
+
expect(Sequel::Unicache.unicache_suspended?).to be false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'expire when delete' do
|
189
|
+
let!(:user) { User[user_id] }
|
190
|
+
|
191
|
+
it 'should expire cache' do
|
192
|
+
cache = memcache.get "User:id:#{user.id}"
|
193
|
+
expect(cache).not_to be_nil
|
194
|
+
user.destroy
|
195
|
+
cache = memcache.get "User:id:#{user.id}"
|
196
|
+
expect(cache).to be_nil
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'should expire cache' do
|
200
|
+
User.instance_exec { unicache :username }
|
201
|
+
user = User.select(:id).first
|
202
|
+
user.destroy
|
203
|
+
cache = memcache.get "User:username:#{user.username}"
|
204
|
+
expect(cache).to be_nil
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'should not expire cache until transaction is committed' do
|
208
|
+
User.db.transaction auto_savepoint: true do
|
209
|
+
user.destroy
|
210
|
+
cache = memcache.get "User:id:#{user.id}"
|
211
|
+
expect(cache).not_to be_nil
|
212
|
+
end
|
213
|
+
cache = memcache.get "User:id:#{user.id}"
|
214
|
+
expect(cache).to be_nil
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should not expire cache even if unicache is not enabled for that key' do
|
218
|
+
User.unicache_for(:id).enabled = false
|
219
|
+
user.destroy
|
220
|
+
cache = memcache.get "User:id:#{user.id}"
|
221
|
+
expect(cache).not_to be_nil
|
222
|
+
expect(Marshal.load(cache)).to eq user.values
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should not expire cache even if unicache is not enabled' do
|
226
|
+
Sequel::Unicache.disable
|
227
|
+
user.destroy
|
228
|
+
cache = memcache.get "User:id:#{user.id}"
|
229
|
+
expect(cache).not_to be_nil
|
230
|
+
expect(Marshal.load(cache)).to eq user.values
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should still expire cache even if read-through is suspended' do
|
234
|
+
Sequel::Unicache.suspend_unicache { user.destroy }
|
235
|
+
cache = memcache.get "User:id:#{user.id}"
|
236
|
+
expect(cache).to be_nil
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context 'expire by #expire_unicache' do
|
241
|
+
let(:user) { User[user_id] }
|
242
|
+
|
243
|
+
before :each do
|
244
|
+
User.instance_exec do
|
245
|
+
unicache :username
|
246
|
+
unicache :company_name, :department, :employee_id
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'should expire all keys' do
|
251
|
+
cache = memcache.get "User:id:#{user.id}"
|
252
|
+
expect(cache).not_to be_nil
|
253
|
+
cache = memcache.get "User:username:bachue@gmail.com"
|
254
|
+
expect(cache).not_to be_nil
|
255
|
+
cache = memcache.get "User:company_name:EMC:department:Mozy:employee_id:12345"
|
256
|
+
expect(cache).not_to be_nil
|
257
|
+
user.expire_unicache
|
258
|
+
cache = memcache.get "User:id:#{user.id}"
|
259
|
+
expect(cache).to be_nil
|
260
|
+
cache = memcache.get "User:username:bachue@gmail.com"
|
261
|
+
expect(cache).to be_nil
|
262
|
+
cache = memcache.get "User:company_name:EMC:department:Mozy:employee_id:12345"
|
263
|
+
expect(cache).to be_nil
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'should expire all keys even unicache is disabled' do
|
267
|
+
Sequel::Unicache.disable
|
268
|
+
user.expire_unicache
|
269
|
+
cache = memcache.get "User:id:#{user.id}"
|
270
|
+
expect(cache).to be_nil
|
271
|
+
cache = memcache.get "User:username:bachue@gmail.com"
|
272
|
+
expect(cache).to be_nil
|
273
|
+
cache = memcache.get "User:company_name:EMC:department:Mozy:employee_id:12345"
|
274
|
+
expect(cache).to be_nil
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|