appsignal 2.11.0.alpha.2-java → 2.11.0.beta.5-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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.semaphore/semaphore.yml +94 -10
- data/CHANGELOG.md +31 -1
- data/README.md +4 -4
- data/Rakefile +16 -4
- data/appsignal.gemspec +1 -1
- data/build_matrix.yml +7 -3
- data/ext/Rakefile +2 -0
- data/ext/agent.yml +19 -19
- data/ext/base.rb +7 -0
- data/ext/extconf.rb +2 -0
- data/gemfiles/rails-4.2.gemfile +9 -2
- data/gemfiles/rails-5.0.gemfile +1 -0
- data/gemfiles/rails-5.1.gemfile +1 -0
- data/gemfiles/rails-5.2.gemfile +1 -0
- data/gemfiles/rails-6.0.gemfile +1 -0
- data/gemfiles/resque-1.gemfile +7 -0
- data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
- data/lib/appsignal.rb +1 -0
- data/lib/appsignal/auth_check.rb +4 -2
- data/lib/appsignal/cli/diagnose.rb +1 -1
- data/lib/appsignal/config.rb +35 -2
- data/lib/appsignal/extension.rb +6 -5
- data/lib/appsignal/extension/jruby.rb +6 -5
- data/lib/appsignal/hooks.rb +25 -0
- data/lib/appsignal/hooks/active_job.rb +137 -0
- data/lib/appsignal/hooks/puma.rb +0 -1
- data/lib/appsignal/hooks/resque.rb +60 -0
- data/lib/appsignal/hooks/sidekiq.rb +17 -94
- data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
- data/lib/appsignal/integrations/que.rb +1 -1
- data/lib/appsignal/integrations/resque.rb +9 -12
- data/lib/appsignal/integrations/resque_active_job.rb +9 -32
- data/lib/appsignal/probes.rb +7 -0
- data/lib/appsignal/probes/puma.rb +1 -1
- data/lib/appsignal/probes/sidekiq.rb +3 -1
- data/lib/appsignal/transaction.rb +10 -0
- data/lib/appsignal/utils/deprecation_message.rb +6 -2
- data/lib/appsignal/version.rb +1 -1
- data/spec/lib/appsignal/auth_check_spec.rb +23 -0
- data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
- data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
- data/spec/lib/appsignal/cli/diagnose_spec.rb +42 -0
- data/spec/lib/appsignal/config_spec.rb +21 -0
- data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
- data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
- data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
- data/spec/lib/appsignal/hooks/delayed_job_spec.rb +3 -14
- data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +222 -268
- data/spec/lib/appsignal/hooks_spec.rb +57 -0
- data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
- data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -179
- data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
- data/spec/lib/appsignal/marker_spec.rb +1 -1
- data/spec/lib/appsignal/probes/sidekiq_spec.rb +10 -7
- data/spec/lib/appsignal/transaction_spec.rb +5 -7
- data/spec/spec_helper.rb +5 -0
- data/spec/support/helpers/action_mailer_helpers.rb +25 -0
- data/spec/support/helpers/config_helpers.rb +3 -2
- data/spec/support/helpers/dependency_helper.rb +9 -2
- data/spec/support/helpers/transaction_helpers.rb +6 -0
- data/spec/support/stubs/sidekiq/api.rb +1 -1
- data/spec/support/testing.rb +19 -19
- metadata +16 -4
@@ -78,6 +78,63 @@ describe Appsignal::Hooks do
|
|
78
78
|
expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
|
79
79
|
Appsignal::Hooks.hooks.delete(:mock_error_hook)
|
80
80
|
end
|
81
|
+
|
82
|
+
describe "missing constants" do
|
83
|
+
let(:err_stream) { std_stream }
|
84
|
+
let(:stderr) { err_stream.read }
|
85
|
+
let(:log_stream) { std_stream }
|
86
|
+
let(:log) { log_contents(log_stream) }
|
87
|
+
before do
|
88
|
+
Appsignal.logger = test_logger(log_stream)
|
89
|
+
end
|
90
|
+
|
91
|
+
def call_constant(&block)
|
92
|
+
capture_std_streams(std_stream, err_stream, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "SidekiqProbe" do
|
96
|
+
it "logs a deprecation message and returns the new constant" do
|
97
|
+
constant = call_constant { Appsignal::Hooks::SidekiqProbe }
|
98
|
+
|
99
|
+
expect(constant).to eql(Appsignal::Probes::SidekiqProbe)
|
100
|
+
expect(constant.name).to eql("Appsignal::Probes::SidekiqProbe")
|
101
|
+
|
102
|
+
deprecation_message =
|
103
|
+
"The constant Appsignal::Hooks::SidekiqProbe has been deprecated. " \
|
104
|
+
"Please update the constant name to Appsignal::Probes::SidekiqProbe " \
|
105
|
+
"in the following file to remove this message.\n#{__FILE__}:"
|
106
|
+
expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
|
107
|
+
expect(log).to contains_log :warn, deprecation_message
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "PumaProbe" do
|
112
|
+
it "logs a deprecation message and returns the new constant" do
|
113
|
+
constant = call_constant { Appsignal::Hooks::PumaProbe }
|
114
|
+
|
115
|
+
expect(constant).to eql(Appsignal::Probes::PumaProbe)
|
116
|
+
expect(constant.name).to eql("Appsignal::Probes::PumaProbe")
|
117
|
+
|
118
|
+
deprecation_message =
|
119
|
+
"The constant Appsignal::Hooks::PumaProbe has been deprecated. " \
|
120
|
+
"Please update the constant name to Appsignal::Probes::PumaProbe " \
|
121
|
+
"in the following file to remove this message.\n#{__FILE__}:"
|
122
|
+
expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
|
123
|
+
expect(log).to contains_log :warn, deprecation_message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "other constant" do
|
128
|
+
it "raises a NameError like Ruby normally does" do
|
129
|
+
expect do
|
130
|
+
call_constant { Appsignal::Hooks::Unknown }
|
131
|
+
end.to raise_error(NameError)
|
132
|
+
|
133
|
+
expect(stderr).to be_empty
|
134
|
+
expect(log).to be_empty
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
81
138
|
end
|
82
139
|
|
83
140
|
describe Appsignal::Hooks::Helpers do
|
@@ -14,7 +14,6 @@ if DependencyHelper.que_present?
|
|
14
14
|
:error_count => 0
|
15
15
|
}
|
16
16
|
end
|
17
|
-
|
18
17
|
let(:env) do
|
19
18
|
{
|
20
19
|
:class => "MyQueJob",
|
@@ -29,7 +28,6 @@ if DependencyHelper.que_present?
|
|
29
28
|
:params => %w[1 birds]
|
30
29
|
}
|
31
30
|
end
|
32
|
-
|
33
31
|
let(:job) do
|
34
32
|
Class.new(::Que::Job) do
|
35
33
|
def run(*args)
|
@@ -37,7 +35,6 @@ if DependencyHelper.que_present?
|
|
37
35
|
end
|
38
36
|
end
|
39
37
|
let(:instance) { job.new(job_attrs) }
|
40
|
-
|
41
38
|
before do
|
42
39
|
allow(Que).to receive(:execute)
|
43
40
|
|
@@ -46,10 +43,14 @@ if DependencyHelper.que_present?
|
|
46
43
|
end
|
47
44
|
around { |example| keep_transactions { example.run } }
|
48
45
|
|
46
|
+
def perform_job(job)
|
47
|
+
job._run
|
48
|
+
end
|
49
|
+
|
49
50
|
context "success" do
|
50
51
|
it "creates a transaction for a job" do
|
51
52
|
expect do
|
52
|
-
instance
|
53
|
+
perform_job(instance)
|
53
54
|
end.to change { created_transactions.length }.by(1)
|
54
55
|
|
55
56
|
expect(last_transaction).to be_completed
|
@@ -95,7 +96,7 @@ if DependencyHelper.que_present?
|
|
95
96
|
|
96
97
|
expect do
|
97
98
|
expect do
|
98
|
-
instance
|
99
|
+
perform_job(instance)
|
99
100
|
end.to raise_error(ExampleException)
|
100
101
|
end.to change { created_transactions.length }.by(1)
|
101
102
|
|
@@ -130,7 +131,7 @@ if DependencyHelper.que_present?
|
|
130
131
|
it "reports errors and not re-raise them" do
|
131
132
|
allow(instance).to receive(:run).and_raise(error)
|
132
133
|
|
133
|
-
expect { instance
|
134
|
+
expect { perform_job(instance) }.to change { created_transactions.length }.by(1)
|
134
135
|
|
135
136
|
expect(last_transaction).to be_completed
|
136
137
|
transaction_hash = last_transaction.to_h
|
@@ -156,6 +157,24 @@ if DependencyHelper.que_present?
|
|
156
157
|
)
|
157
158
|
end
|
158
159
|
end
|
160
|
+
|
161
|
+
context "when action set in job" do
|
162
|
+
let(:job) do
|
163
|
+
Class.new(::Que::Job) do
|
164
|
+
def run(*_args)
|
165
|
+
Appsignal.set_action("MyCustomJob#perform")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "uses the custom action" do
|
171
|
+
perform_job(instance)
|
172
|
+
|
173
|
+
expect(last_transaction).to be_completed
|
174
|
+
transaction_hash = last_transaction.to_h
|
175
|
+
expect(transaction_hash).to include("action" => "MyCustomJob#perform")
|
176
|
+
end
|
177
|
+
end
|
159
178
|
end
|
160
179
|
end
|
161
180
|
end
|
@@ -1,187 +1,28 @@
|
|
1
|
-
|
2
|
-
require "active_job"
|
3
|
-
require File.expand_path("lib/appsignal/integrations/resque_active_job.rb")
|
1
|
+
require "appsignal/integrations/resque_active_job"
|
4
2
|
|
5
|
-
|
6
|
-
|
3
|
+
describe "Legacy Resque ActiveJob integration" do
|
4
|
+
let(:err_stream) { std_stream }
|
5
|
+
let(:stderr) { err_stream.read }
|
6
|
+
let(:log_stream) { std_stream }
|
7
|
+
let(:log) { log_contents(log_stream) }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
describe Appsignal::Integrations::ResqueActiveJobPlugin do
|
13
|
-
let(:args) { "argument" }
|
14
|
-
let(:job) { TestActiveJob.new(args) }
|
15
|
-
before { start_agent }
|
16
|
-
|
17
|
-
def perform
|
18
|
-
keep_transactions do
|
19
|
-
job.perform_now
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context "without error" do
|
24
|
-
it "creates a new transaction" do
|
25
|
-
expect { perform }.to change { created_transactions.length }.by(1)
|
26
|
-
|
27
|
-
expect(last_transaction.to_h).to include(
|
28
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
29
|
-
"action" => "TestActiveJob#perform",
|
30
|
-
"error" => nil,
|
31
|
-
"events" => [
|
32
|
-
hash_including(
|
33
|
-
"name" => "perform_job.resque",
|
34
|
-
"title" => "",
|
35
|
-
"body" => "",
|
36
|
-
"body_format" => Appsignal::EventFormatter::DEFAULT,
|
37
|
-
"count" => 1,
|
38
|
-
"duration" => kind_of(Float)
|
39
|
-
)
|
40
|
-
],
|
41
|
-
"sample_data" => hash_including(
|
42
|
-
"params" => ["argument"],
|
43
|
-
"metadata" => {
|
44
|
-
"id" => kind_of(String),
|
45
|
-
"queue" => "default"
|
46
|
-
}
|
47
|
-
)
|
48
|
-
)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context "with error" do
|
53
|
-
let(:job) do
|
54
|
-
class BrokenTestActiveJob < ActiveJob::Base
|
55
|
-
include Appsignal::Integrations::ResqueActiveJobPlugin
|
56
|
-
|
57
|
-
def perform(_)
|
58
|
-
raise ExampleException, "my error message"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
BrokenTestActiveJob.new(args)
|
63
|
-
end
|
64
|
-
|
65
|
-
it "creates a new transaction with an error" do
|
66
|
-
expect do
|
67
|
-
expect { perform }.to raise_error(ExampleException, "my error message")
|
68
|
-
end.to change { created_transactions.length }.by(1)
|
9
|
+
it "logs and prints a deprecation message on extend" do
|
10
|
+
Appsignal.logger = test_logger(log_stream)
|
69
11
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
"error" => {
|
74
|
-
"name" => "ExampleException",
|
75
|
-
"message" => "my error message",
|
76
|
-
"backtrace" => kind_of(String)
|
77
|
-
},
|
78
|
-
"sample_data" => hash_including(
|
79
|
-
"params" => ["argument"],
|
80
|
-
"metadata" => {
|
81
|
-
"id" => kind_of(String),
|
82
|
-
"queue" => "default"
|
83
|
-
}
|
84
|
-
)
|
85
|
-
)
|
12
|
+
capture_std_streams(std_stream, err_stream) do
|
13
|
+
Class.new do
|
14
|
+
include Appsignal::Integrations::ResqueActiveJobPlugin
|
86
15
|
end
|
87
16
|
end
|
88
17
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
it "truncates large argument values" do
|
99
|
-
perform
|
100
|
-
expect(last_transaction.to_h).to include(
|
101
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
102
|
-
"action" => "TestActiveJob#perform",
|
103
|
-
"error" => nil,
|
104
|
-
"sample_data" => hash_including(
|
105
|
-
"params" => ["foo" => "Foo", "bar" => "#{"a" * 2000}..."],
|
106
|
-
"metadata" => {
|
107
|
-
"id" => kind_of(String),
|
108
|
-
"queue" => "default"
|
109
|
-
}
|
110
|
-
)
|
111
|
-
)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
context "with parameter filtering" do
|
116
|
-
let(:args) do
|
117
|
-
{
|
118
|
-
:foo => "Foo",
|
119
|
-
:bar => "Bar"
|
120
|
-
}
|
121
|
-
end
|
122
|
-
before { Appsignal.config[:filter_parameters] = ["foo"] }
|
123
|
-
|
124
|
-
it "filters selected arguments" do
|
125
|
-
perform
|
126
|
-
expect(last_transaction.to_h).to include(
|
127
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
128
|
-
"action" => "TestActiveJob#perform",
|
129
|
-
"error" => nil,
|
130
|
-
"sample_data" => hash_including(
|
131
|
-
"params" => ["foo" => "[FILTERED]", "bar" => "Bar"],
|
132
|
-
"metadata" => {
|
133
|
-
"id" => kind_of(String),
|
134
|
-
"queue" => "default"
|
135
|
-
}
|
136
|
-
)
|
137
|
-
)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
context "without queue time" do
|
143
|
-
it "does not add queue time to transaction" do
|
144
|
-
# TODO: Not available in transaction.to_h yet.
|
145
|
-
# https://github.com/appsignal/appsignal-agent/issues/293
|
146
|
-
expect(Appsignal).to receive(:monitor_single_transaction).with(
|
147
|
-
"perform_job.resque",
|
148
|
-
a_hash_including(:queue_start => nil)
|
149
|
-
).and_call_original
|
150
|
-
|
151
|
-
perform
|
152
|
-
expect(last_transaction.to_h).to include(
|
153
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
154
|
-
"action" => "TestActiveJob#perform",
|
155
|
-
"events" => [
|
156
|
-
hash_including("name" => "perform_job.resque")
|
157
|
-
]
|
158
|
-
)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
if DependencyHelper.rails6_present?
|
163
|
-
context "with queue time" do
|
164
|
-
it "adds queue time to transction" do
|
165
|
-
queue_start = "2017-01-01 10:01:00UTC"
|
166
|
-
queue_start_time = Time.parse(queue_start)
|
167
|
-
# TODO: Not available in transaction.to_h yet.
|
168
|
-
# https://github.com/appsignal/appsignal-agent/issues/293
|
169
|
-
expect(Appsignal).to receive(:monitor_single_transaction).with(
|
170
|
-
"perform_job.resque",
|
171
|
-
a_hash_including(:queue_start => queue_start_time)
|
172
|
-
).and_call_original
|
173
|
-
job.enqueued_at = queue_start
|
174
|
-
|
175
|
-
perform
|
176
|
-
expect(last_transaction.to_h).to include(
|
177
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
178
|
-
"action" => "TestActiveJob#perform",
|
179
|
-
"events" => [
|
180
|
-
hash_including("name" => "perform_job.resque")
|
181
|
-
]
|
182
|
-
)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
18
|
+
deprecation_message =
|
19
|
+
"The AppSignal ResqueActiveJobPlugin is deprecated and does " \
|
20
|
+
"nothing on extend. In this version of the AppSignal Ruby gem " \
|
21
|
+
"the integration with Resque is automatic on all Resque workers. " \
|
22
|
+
"Please remove the following line from this file to remove this " \
|
23
|
+
"message: include Appsignal::Integrations::ResqueActiveJobPlugin\n" \
|
24
|
+
"#{__FILE__}:"
|
25
|
+
expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
|
26
|
+
expect(log).to contains_log :warn, deprecation_message
|
186
27
|
end
|
187
28
|
end
|
@@ -1,93 +1,28 @@
|
|
1
|
-
|
2
|
-
describe "Resque integration" do
|
3
|
-
let(:file) { File.expand_path("lib/appsignal/integrations/resque.rb") }
|
1
|
+
require "appsignal/integrations/resque"
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
describe "Legacy Resque integration" do
|
4
|
+
let(:err_stream) { std_stream }
|
5
|
+
let(:stderr) { err_stream.read }
|
6
|
+
let(:log_stream) { std_stream }
|
7
|
+
let(:log) { log_contents(log_stream) }
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
it "logs and prints a deprecation message on extend" do
|
10
|
+
Appsignal.logger = test_logger(log_stream)
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
class BrokenTestJob
|
18
|
-
extend Appsignal::Integrations::ResquePlugin
|
19
|
-
|
20
|
-
def self.perform
|
21
|
-
raise ExampleException, "my error message"
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
describe :around_perform_resque_plugin do
|
27
|
-
let(:job) { ::Resque::Job.new("default", "class" => "TestJob") }
|
28
|
-
before { expect(Appsignal).to receive(:stop) }
|
29
|
-
|
30
|
-
context "without exception" do
|
31
|
-
it "creates a new transaction" do
|
32
|
-
expect do
|
33
|
-
keep_transactions { job.perform }
|
34
|
-
end.to change { created_transactions.length }.by(1)
|
35
|
-
|
36
|
-
expect(last_transaction).to be_completed
|
37
|
-
expect(last_transaction.to_h).to include(
|
38
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
39
|
-
"action" => "TestJob#perform",
|
40
|
-
"error" => nil,
|
41
|
-
"events" => [
|
42
|
-
hash_including(
|
43
|
-
"name" => "perform_job.resque",
|
44
|
-
"title" => "",
|
45
|
-
"body" => "",
|
46
|
-
"body_format" => Appsignal::EventFormatter::DEFAULT,
|
47
|
-
"count" => 1,
|
48
|
-
"duration" => kind_of(Float)
|
49
|
-
)
|
50
|
-
]
|
51
|
-
)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
context "with exception" do
|
56
|
-
let(:job) { ::Resque::Job.new("default", "class" => "BrokenTestJob") }
|
57
|
-
|
58
|
-
def perform
|
59
|
-
keep_transactions do
|
60
|
-
expect do
|
61
|
-
job.perform
|
62
|
-
end.to raise_error(ExampleException, "my error message")
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
it "sets the exception on the transaction" do
|
67
|
-
expect do
|
68
|
-
perform
|
69
|
-
end.to change { created_transactions.length }.by(1)
|
70
|
-
|
71
|
-
expect(last_transaction).to be_completed
|
72
|
-
expect(last_transaction.to_h).to include(
|
73
|
-
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
74
|
-
"action" => "BrokenTestJob#perform",
|
75
|
-
"error" => {
|
76
|
-
"name" => "ExampleException",
|
77
|
-
"message" => "my error message",
|
78
|
-
"backtrace" => kind_of(String)
|
79
|
-
}
|
80
|
-
)
|
81
|
-
end
|
82
|
-
end
|
12
|
+
capture_std_streams(std_stream, err_stream) do
|
13
|
+
Class.new do
|
14
|
+
extend Appsignal::Integrations::ResquePlugin
|
83
15
|
end
|
84
16
|
end
|
85
17
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
18
|
+
deprecation_message =
|
19
|
+
"The AppSignal ResquePlugin is deprecated and does " \
|
20
|
+
"nothing on extend. In this version of the AppSignal Ruby gem " \
|
21
|
+
"the integration with Resque is automatic on all Resque workers. " \
|
22
|
+
"Please remove the following line from this file to remove this " \
|
23
|
+
"message: extend Appsignal::Integrations::ResquePlugin\n" \
|
24
|
+
"#{__FILE__}:"
|
25
|
+
expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
|
26
|
+
expect(log).to contains_log :warn, deprecation_message
|
92
27
|
end
|
93
28
|
end
|