identity_cache 0.0.3 → 0.0.4
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 +13 -5
- data/.travis.yml +4 -2
- data/CHANGELOG +5 -0
- data/README.md +9 -1
- data/Rakefile +5 -1
- data/identity_cache.gemspec +14 -6
- data/lib/identity_cache.rb +17 -34
- data/lib/identity_cache/cache_hash.rb +36 -0
- data/lib/identity_cache/cache_key_generation.rb +34 -6
- data/lib/identity_cache/configuration_dsl.rb +4 -6
- data/lib/identity_cache/memoized_cache_proxy.rb +19 -17
- data/lib/identity_cache/query_api.rb +105 -11
- data/lib/identity_cache/version.rb +2 -1
- data/performance/cache_runner.rb +44 -15
- data/performance/cpu.rb +3 -0
- data/performance/externals.rb +4 -0
- data/performance/profile.rb +4 -0
- data/test/attribute_cache_test.rb +5 -3
- data/test/cache_hash_test.rb +16 -0
- data/test/fetch_multi_test.rb +32 -13
- data/test/fetch_multi_with_batched_associations_test.rb +6 -4
- data/test/fetch_test.rb +28 -7
- data/test/fixtures/serialized_record +0 -0
- data/test/helpers/active_record_objects.rb +25 -1
- data/test/helpers/database_connection.rb +7 -6
- data/test/helpers/serialization_format.rb +38 -0
- data/test/helpers/update_serialization_format.rb +28 -0
- data/test/index_cache_test.rb +5 -3
- data/test/normalized_has_many_test.rb +10 -0
- data/test/save_test.rb +16 -14
- data/test/schema_change_test.rb +16 -0
- data/test/serialization_format_change_test.rb +16 -0
- data/test/test_helper.rb +1 -1
- metadata +51 -40
@@ -4,6 +4,11 @@ module IdentityCache
|
|
4
4
|
|
5
5
|
included do |base|
|
6
6
|
base.private_class_method :require_if_necessary
|
7
|
+
base.private_class_method :coder_from_record
|
8
|
+
base.private_class_method :record_from_coder
|
9
|
+
base.private_class_method :set_embedded_association
|
10
|
+
base.private_class_method :get_embedded_association
|
11
|
+
base.private_class_method :add_cached_associations_to_coder
|
7
12
|
base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
8
13
|
private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association,
|
9
14
|
:populate_denormalized_cached_association
|
@@ -27,7 +32,9 @@ module IdentityCache
|
|
27
32
|
if IdentityCache.should_cache?
|
28
33
|
|
29
34
|
require_if_necessary do
|
30
|
-
object =
|
35
|
+
object = nil
|
36
|
+
coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
|
37
|
+
object ||= record_from_coder(coder)
|
31
38
|
IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
|
32
39
|
object
|
33
40
|
end
|
@@ -41,7 +48,7 @@ module IdentityCache
|
|
41
48
|
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
|
42
49
|
# if id is not in the cache or the db.
|
43
50
|
def fetch(id)
|
44
|
-
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.
|
51
|
+
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
|
45
52
|
end
|
46
53
|
|
47
54
|
# Default fetcher added to the model on inclusion, if behaves like
|
@@ -55,14 +62,14 @@ module IdentityCache
|
|
55
62
|
cache_keys = ids.map {|id| rails_cache_key(id) }
|
56
63
|
key_to_id_map = Hash[ cache_keys.zip(ids) ]
|
57
64
|
|
58
|
-
|
65
|
+
coders_by_key = IdentityCache.fetch_multi(*cache_keys) do |unresolved_keys|
|
59
66
|
ids = unresolved_keys.map {|key| key_to_id_map[key] }
|
60
67
|
records = find_batch(ids, options)
|
61
68
|
records.compact.each(&:populate_association_caches)
|
62
|
-
records
|
69
|
+
records.map {|record| coder_from_record(record) }
|
63
70
|
end
|
64
71
|
|
65
|
-
records = cache_keys.map {|key|
|
72
|
+
records = cache_keys.map {|key| record_from_coder(coders_by_key[key]) }.compact
|
66
73
|
prefetch_associations(options[:includes], records) if options[:includes]
|
67
74
|
|
68
75
|
records
|
@@ -73,6 +80,82 @@ module IdentityCache
|
|
73
80
|
end
|
74
81
|
end
|
75
82
|
|
83
|
+
def record_from_coder(coder) #:nodoc:
|
84
|
+
if coder.present? && coder.has_key?(:class)
|
85
|
+
record = coder[:class].allocate
|
86
|
+
unless coder[:class].serialized_attributes.empty?
|
87
|
+
coder = coder.dup
|
88
|
+
coder['attributes'] = coder['attributes'].dup
|
89
|
+
end
|
90
|
+
if record.class._initialize_callbacks.empty?
|
91
|
+
record.instance_eval do
|
92
|
+
@attributes = self.class.initialize_attributes(coder['attributes'])
|
93
|
+
@relation = nil
|
94
|
+
|
95
|
+
@attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
|
96
|
+
@association_cache = {}
|
97
|
+
@aggregation_cache = {}
|
98
|
+
@readonly = @destroyed = @marked_for_destruction = false
|
99
|
+
@new_record = false
|
100
|
+
end
|
101
|
+
else
|
102
|
+
record.init_with(coder)
|
103
|
+
end
|
104
|
+
|
105
|
+
coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
|
106
|
+
coder[:normalized_has_many].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:normalized_has_many)
|
107
|
+
record
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_embedded_association(record, association_name, coder_or_array) #:nodoc:
|
112
|
+
value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
|
113
|
+
nil
|
114
|
+
elsif (reflection = record.class.reflect_on_association(association_name)).collection?
|
115
|
+
association = reflection.association_class.new(record, reflection)
|
116
|
+
association.target = coder_or_array.map {|e| record_from_coder(e) }
|
117
|
+
association.target.each {|e| association.set_inverse_instance(e) }
|
118
|
+
association.proxy
|
119
|
+
else
|
120
|
+
record_from_coder(coder_or_array)
|
121
|
+
end
|
122
|
+
variable_name = record.class.all_embedded_associations[association_name][:records_variable_name]
|
123
|
+
record.instance_variable_set(:"@#{variable_name}", IdentityCache.map_cached_nil_for(value))
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_embedded_association(record, association, options) #:nodoc:
|
127
|
+
embedded_variable = record.instance_variable_get(:"@#{options[:records_variable_name]}")
|
128
|
+
if IdentityCache.unmap_cached_nil_for(embedded_variable).nil?
|
129
|
+
nil
|
130
|
+
elsif record.class.reflect_on_association(association).collection?
|
131
|
+
embedded_variable.map {|e| coder_from_record(e) }
|
132
|
+
else
|
133
|
+
coder_from_record(embedded_variable)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def coder_from_record(record) #:nodoc:
|
138
|
+
unless record.nil?
|
139
|
+
coder = {:class => record.class }
|
140
|
+
record.encode_with(coder)
|
141
|
+
add_cached_associations_to_coder(record, coder)
|
142
|
+
coder
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def add_cached_associations_to_coder(record, coder)
|
147
|
+
if record.class.respond_to?(:all_embedded_associations) && record.class.all_embedded_associations.present?
|
148
|
+
coder[:associations] = record.class.all_embedded_associations.each_with_object({}) do |(name, options), hash|
|
149
|
+
hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
if record.class.respond_to?(:cached_has_manys) && record.class.cached_has_manys.present?
|
153
|
+
coder[:normalized_has_many] = record.class.cached_has_manys.each_with_object({}) do |(name, options), hash|
|
154
|
+
hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
76
159
|
def require_if_necessary #:nodoc:
|
77
160
|
# mem_cache_store returns raw value if unmarshal fails
|
78
161
|
rval = yield
|
@@ -97,6 +180,12 @@ module IdentityCache
|
|
97
180
|
end
|
98
181
|
end
|
99
182
|
|
183
|
+
def all_embedded_associations
|
184
|
+
all_cached_associations.select do |cached_association, options|
|
185
|
+
options[:embed].present?
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
100
189
|
def all_cached_associations
|
101
190
|
(cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
|
102
191
|
end
|
@@ -264,13 +353,18 @@ module IdentityCache
|
|
264
353
|
|
265
354
|
def expire_primary_index # :nodoc:
|
266
355
|
return unless self.class.primary_cache_index_enabled
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
356
|
+
|
357
|
+
IdentityCache.logger.debug do
|
358
|
+
extra_keys =
|
359
|
+
if respond_to?(:updated_at)
|
360
|
+
old_updated_at = old_values_for_fields([:updated_at]).first
|
361
|
+
"expiring_last_updated_at=#{old_updated_at}"
|
362
|
+
else
|
363
|
+
""
|
364
|
+
end
|
365
|
+
|
366
|
+
"[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
|
272
367
|
end
|
273
|
-
IdentityCache.logger.debug { "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}" }
|
274
368
|
|
275
369
|
IdentityCache.cache.delete(primary_cache_index_key)
|
276
370
|
end
|
data/performance/cache_runner.rb
CHANGED
@@ -37,6 +37,7 @@ def create_database(count)
|
|
37
37
|
# set up associations
|
38
38
|
Record.cache_has_one :associated
|
39
39
|
Record.cache_has_many :associated_records, :embed => true
|
40
|
+
Record.cache_has_many :normalized_associated_records, :embed => false
|
40
41
|
Record.cache_index :title, :unique => :true
|
41
42
|
AssociatedRecord.cache_has_many :deeply_associated_records, :embed => true
|
42
43
|
|
@@ -54,6 +55,7 @@ def create_database(count)
|
|
54
55
|
a.associated_records
|
55
56
|
(1..5).each do |j|
|
56
57
|
a.associated_records << AssociatedRecord.new(name: "Has Many #{j} for #{i}")
|
58
|
+
a.normalized_associated_records << NormalizedAssociatedRecord.new(name: "Normalized Has Many #{j} for #{i}")
|
57
59
|
end
|
58
60
|
a.save
|
59
61
|
end
|
@@ -74,49 +76,76 @@ end
|
|
74
76
|
|
75
77
|
class FindRunner < CacheRunner
|
76
78
|
def run
|
77
|
-
|
78
|
-
@count.times do
|
79
|
+
(1..@count).each do |i|
|
79
80
|
::Record.find(i)
|
80
|
-
i+=1
|
81
81
|
end
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
|
85
|
+
module MissRunner
|
86
86
|
def prepare
|
87
87
|
IdentityCache.cache.clear
|
88
88
|
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class FetchMissRunner < CacheRunner
|
92
|
+
include MissRunner
|
89
93
|
|
90
94
|
def run
|
91
|
-
|
92
|
-
@count.times do
|
95
|
+
(1..@count).each do |i|
|
93
96
|
rec = ::Record.fetch(i)
|
94
97
|
rec.fetch_associated
|
95
98
|
rec.fetch_associated_records
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
96
102
|
|
97
|
-
|
103
|
+
class DoubleFetchMissRunner < CacheRunner
|
104
|
+
include MissRunner
|
105
|
+
|
106
|
+
def run
|
107
|
+
(1..@count).each do |i|
|
108
|
+
rec = ::Record.fetch(i)
|
109
|
+
rec.fetch_associated
|
110
|
+
rec.fetch_associated_records
|
111
|
+
rec.fetch_normalized_associated_records
|
98
112
|
end
|
99
113
|
end
|
100
114
|
end
|
101
115
|
|
102
|
-
|
116
|
+
module HitRunner
|
103
117
|
def prepare
|
104
118
|
IdentityCache.cache.clear
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
i+=1
|
119
|
+
(1..@count).each do |i|
|
120
|
+
rec = ::Record.fetch(i)
|
121
|
+
rec.fetch_normalized_associated_records
|
109
122
|
end
|
110
123
|
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class FetchHitRunner < CacheRunner
|
127
|
+
include HitRunner
|
128
|
+
|
129
|
+
def run
|
130
|
+
(1..@count).each do |i|
|
131
|
+
rec = ::Record.fetch(i)
|
132
|
+
# these should all be no cost
|
133
|
+
rec.fetch_associated
|
134
|
+
rec.fetch_associated_records
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class DoubleFetchHitRunner < CacheRunner
|
140
|
+
include HitRunner
|
111
141
|
|
112
142
|
def run
|
113
|
-
|
114
|
-
@count.times do
|
143
|
+
(1..@count).each do |i|
|
115
144
|
rec = ::Record.fetch(i)
|
116
145
|
# these should all be no cost
|
117
146
|
rec.fetch_associated
|
118
147
|
rec.fetch_associated_records
|
119
|
-
|
148
|
+
rec.fetch_normalized_associated_records
|
120
149
|
end
|
121
150
|
end
|
122
151
|
end
|
data/performance/cpu.rb
CHANGED
data/performance/externals.rb
CHANGED
data/performance/profile.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class AttributeCacheTest < IdentityCache::TestCase
|
4
|
+
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE
|
5
|
+
|
4
6
|
def setup
|
5
7
|
super
|
6
8
|
AssociatedRecord.cache_attribute :name
|
@@ -8,8 +10,8 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
8
10
|
|
9
11
|
@parent = Record.create!(:title => 'bob')
|
10
12
|
@record = @parent.associated_records.create!(:name => 'foo')
|
11
|
-
@name_attribute_key = "
|
12
|
-
@blob_key = "
|
13
|
+
@name_attribute_key = "#{NAMESPACE}attribute:AssociatedRecord:name:id:#{cache_hash(@record.id.to_s)}"
|
14
|
+
@blob_key = "#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:1"
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_attribute_values_are_returned_on_cache_hits
|
@@ -57,7 +59,7 @@ class AttributeCacheTest < IdentityCache::TestCase
|
|
57
59
|
end
|
58
60
|
|
59
61
|
def test_cached_attribute_values_are_expired_from_the_cache_when_a_new_record_is_saved
|
60
|
-
IdentityCache.cache.expects(:delete).with("
|
62
|
+
IdentityCache.cache.expects(:delete).with("#{NAMESPACE}blob:AssociatedRecord:#{cache_hash("id:integer,name:string,record_id:integer")}:2")
|
61
63
|
@parent.associated_records.create(:name => 'bar')
|
62
64
|
end
|
63
65
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CacheHashTest < IdentityCache::TestCase
|
4
|
+
|
5
|
+
def test_memcache_hash
|
6
|
+
|
7
|
+
prng = Random.new(Time.now.to_i)
|
8
|
+
3.times do
|
9
|
+
random_str = Array.new(200){rand(36).to_s(36)}.join
|
10
|
+
hash_val = IdentityCache.memcache_hash(random_str)
|
11
|
+
assert hash_val
|
12
|
+
assert_kind_of Numeric, hash_val
|
13
|
+
assert_equal 0, (hash_val >> 64)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/test/fetch_multi_test.rb
CHANGED
@@ -1,26 +1,39 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class FetchMultiTest < IdentityCache::TestCase
|
4
|
+
NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE unless const_defined?(:NAMESPACE)
|
5
|
+
|
4
6
|
def setup
|
5
7
|
super
|
6
8
|
@bob = Record.create!(:title => 'bob')
|
7
9
|
@joe = Record.create!(:title => 'joe')
|
8
10
|
@fred = Record.create!(:title => 'fred')
|
9
|
-
@bob_blob_key = "
|
10
|
-
@joe_blob_key = "
|
11
|
-
@fred_blob_key = "
|
12
|
-
@tenth_blob_key = "
|
11
|
+
@bob_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:1"
|
12
|
+
@joe_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:2"
|
13
|
+
@fred_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:3"
|
14
|
+
@tenth_blob_key = "#{NAMESPACE}blob:Record:#{cache_hash("created_at:datetime,id:integer,record_id:integer,title:string,updated_at:datetime")}:10"
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_fetch_multi_with_no_records
|
16
18
|
assert_equal [], Record.fetch_multi
|
17
19
|
end
|
18
20
|
|
21
|
+
def test_fetch_multi_namespace
|
22
|
+
Record.send(:include, SwitchNamespace)
|
23
|
+
bob_blob_key, joe_blob_key, fred_blob_key = [@bob_blob_key, @joe_blob_key, @fred_blob_key].map { |k| "ns:#{k}" }
|
24
|
+
cache_response = {}
|
25
|
+
cache_response[bob_blob_key] = cache_response_for(@bob)
|
26
|
+
cache_response[joe_blob_key] = cache_response_for(@joe)
|
27
|
+
cache_response[fred_blob_key] = cache_response_for(@fred)
|
28
|
+
IdentityCache.cache.expects(:read_multi).with(bob_blob_key, joe_blob_key, fred_blob_key).returns(cache_response)
|
29
|
+
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
30
|
+
end
|
31
|
+
|
19
32
|
def test_fetch_multi_with_all_hits
|
20
33
|
cache_response = {}
|
21
|
-
cache_response[@bob_blob_key] = @bob
|
22
|
-
cache_response[@joe_blob_key] = @joe
|
23
|
-
cache_response[@fred_blob_key] = @fred
|
34
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
35
|
+
cache_response[@joe_blob_key] = cache_response_for(@joe)
|
36
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
24
37
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
25
38
|
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
26
39
|
end
|
@@ -36,9 +49,9 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
36
49
|
|
37
50
|
def test_fetch_multi_with_mixed_hits_and_misses
|
38
51
|
cache_response = {}
|
39
|
-
cache_response[@bob_blob_key] = @bob
|
52
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
40
53
|
cache_response[@joe_blob_key] = nil
|
41
|
-
cache_response[@fred_blob_key] = @fred
|
54
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
42
55
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
|
43
56
|
assert_equal [@bob, @joe, @fred], Record.fetch_multi(@bob.id, @joe.id, @fred.id)
|
44
57
|
end
|
@@ -47,7 +60,7 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
47
60
|
cache_response = {}
|
48
61
|
cache_response[@bob_blob_key] = nil
|
49
62
|
cache_response[@joe_blob_key] = nil
|
50
|
-
cache_response[@fred_blob_key] = @fred
|
63
|
+
cache_response[@fred_blob_key] = cache_response_for(@fred)
|
51
64
|
IdentityCache.cache.expects(:read_multi).with(@bob_blob_key, @fred_blob_key, @joe_blob_key).returns(cache_response)
|
52
65
|
assert_equal [@bob, @fred, @joe], Record.fetch_multi(@bob.id, @fred.id, @joe.id)
|
53
66
|
end
|
@@ -117,8 +130,8 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
117
130
|
|
118
131
|
def test_fetch_multi_doesnt_freeze_keys
|
119
132
|
cache_response = {}
|
120
|
-
cache_response[@bob_blob_key] = @bob
|
121
|
-
cache_response[@joe_blob_key] = @fred
|
133
|
+
cache_response[@bob_blob_key] = cache_response_for(@bob)
|
134
|
+
cache_response[@joe_blob_key] = cache_response_for(@fred)
|
122
135
|
|
123
136
|
IdentityCache.expects(:fetch_multi).with{ |*args| args.none?(&:frozen?) }.returns(cache_response)
|
124
137
|
|
@@ -132,6 +145,12 @@ class FetchMultiTest < IdentityCache::TestCase
|
|
132
145
|
@cache_response[@bob_blob_key] = nil
|
133
146
|
@cache_response[@joe_blob_key] = nil
|
134
147
|
@cache_response[@tenth_blob_key] = nil
|
135
|
-
@cache_response[@fred_blob_key] = @fred
|
148
|
+
@cache_response[@fred_blob_key] = cache_response_for(@fred)
|
149
|
+
end
|
150
|
+
|
151
|
+
def cache_response_for(record)
|
152
|
+
coder = {:class => record.class}
|
153
|
+
record.encode_with(coder)
|
154
|
+
coder
|
136
155
|
end
|
137
156
|
end
|