px-service-client 1.0.1 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -19
- data/lib/px/service/client/base.rb +5 -4
- data/lib/px/service/client/circuit_breaker.rb +38 -23
- data/lib/px/service/client/future.rb +11 -0
- data/lib/px/service/client/version.rb +1 -1
- data/px-service-client.gemspec +1 -0
- data/spec/px/service/client/circuit_breaker_spec.rb +124 -32
- data/spec/px/service/client/future_spec.rb +48 -1
- data/spec/spec_helper.rb +1 -0
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31469d2437619f1f3afec21f42201aa2054ade9b
|
4
|
+
data.tar.gz: 55491249709bd72ab3e90fa9648cb72dee46880b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb3b61faa7e47e838f45db03731effe82ef5c92ca109878bd592ab80869ab8a00706c611dd586d253078cd9c01b48ae28dda04fd650c71a1cfdefe2dafd8419d
|
7
|
+
data.tar.gz: b86dff5d8658f923ae59494edc8314dd2eb371b2e4757ace908598a106c084f4c02ffa878cd05a882bbcc4056df4cbd7f93335d03f67de75b25aaef6415c9bfd
|
data/README.md
CHANGED
@@ -24,8 +24,8 @@ Then use it:
|
|
24
24
|
require 'px-service-client'
|
25
25
|
|
26
26
|
class MyClient
|
27
|
-
include Px::Service::Client
|
28
|
-
include Px::Service::Client
|
27
|
+
include Px::Service::Client::Caching
|
28
|
+
include Px::Service::Client::CircuitBreaker
|
29
29
|
end
|
30
30
|
|
31
31
|
```
|
@@ -37,10 +37,10 @@ This gem includes several common features used in 500px service client libraries
|
|
37
37
|
|
38
38
|
The features are:
|
39
39
|
|
40
|
-
#### Px::Service::Client
|
40
|
+
#### Px::Service::Client::Caching
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
include Px::Service::Client
|
43
|
+
include Px::Service::Client::Caching
|
44
44
|
|
45
45
|
self.cache_client = Dalli::Client.new(...)
|
46
46
|
self.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
|
@@ -55,13 +55,9 @@ to be refreshed probabilistically (rather than on every request).
|
|
55
55
|
*first-resort* means that the cached value is always used, if present. Requests to the service are only made
|
56
56
|
when the cached value is close to expiry.
|
57
57
|
|
58
|
-
#### Px::Service::Client
|
58
|
+
#### Px::Service::Client::CircuitBreaker
|
59
59
|
|
60
60
|
```ruby
|
61
|
-
def call_remote_service() ...
|
62
|
-
|
63
|
-
circuit_method :call_remote_service
|
64
|
-
|
65
61
|
# Optional
|
66
62
|
circuit_handler do |handler|
|
67
63
|
handler.logger = Logger.new(STDOUT)
|
@@ -72,25 +68,21 @@ circuit_handler do |handler|
|
|
72
68
|
end
|
73
69
|
```
|
74
70
|
|
75
|
-
|
76
|
-
`circuit_method` will be wrapped in a circuit breaker that will raise a `Px::Service::ServiceError` if the breaker
|
77
|
-
is open. The circuit will open on any exception from the wrapped method, or if the wrapped method
|
78
|
-
runs for longer than the `invocation_timeout`.
|
71
|
+
Adds a circuit breaker to the client. The circuit will open on any exception from the wrapped method, or if the request runs for longer than the `invocation_timeout`.
|
79
72
|
|
80
|
-
Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error
|
81
|
-
on the caller's part (e.g. an HTTP 4xx error).
|
73
|
+
Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error on the caller's part (e.g. an HTTP 4xx error).
|
82
74
|
|
83
|
-
|
84
|
-
|
75
|
+
Every instance of the class that includes the `CircuitBreaker` concern will share the same circuit state. You should therefore include `Px::Service::Client::CircuitBreaker` in the most-derived class that subclasses
|
76
|
+
`Px::Service::Client::Base`
|
85
77
|
|
86
78
|
This module is based on (and uses) the [Circuit Breaker](https://github.com/wsargent/circuit_breaker) gem by Will Sargent.
|
87
79
|
|
88
|
-
#### Px::Service::Client
|
80
|
+
#### Px::Service::Client::ListResponse
|
89
81
|
|
90
82
|
```ruby
|
91
83
|
def get_something(page, page_size)
|
92
84
|
response = JSON.parse(http_get("http://some/url?p=#{page}&l=#{page_size}"))
|
93
|
-
return Px::Service::Client
|
85
|
+
return Px::Service::Client::ListResponse(page_size, response, "items")
|
94
86
|
end
|
95
87
|
|
96
88
|
```
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Px::Service::Client
|
2
2
|
class Base
|
3
3
|
include Px::Service::Client::Caching
|
4
|
-
include Px::Service::Client::CircuitBreaker
|
5
4
|
cattr_accessor :logger
|
6
5
|
|
7
6
|
private
|
@@ -18,13 +17,16 @@ module Px::Service::Client
|
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
21
|
-
|
20
|
+
##
|
21
|
+
# Make the request
|
22
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
22
23
|
req = Typhoeus::Request.new(
|
23
24
|
uri,
|
24
25
|
method: method,
|
25
26
|
params: query,
|
26
27
|
body: body,
|
27
|
-
headers: headers
|
28
|
+
headers: headers,
|
29
|
+
timeout: timeout)
|
28
30
|
|
29
31
|
start_time = Time.now
|
30
32
|
logger.debug "Making request #{method.to_s.upcase} #{uri}" if logger
|
@@ -36,6 +38,5 @@ module Px::Service::Client
|
|
36
38
|
|
37
39
|
RetriableResponseFuture.new(req)
|
38
40
|
end
|
39
|
-
|
40
41
|
end
|
41
42
|
end
|
@@ -3,10 +3,9 @@ require 'circuit_breaker'
|
|
3
3
|
module Px::Service::Client
|
4
4
|
module CircuitBreaker
|
5
5
|
extend ActiveSupport::Concern
|
6
|
+
include ::CircuitBreaker
|
6
7
|
|
7
8
|
included do
|
8
|
-
include ::CircuitBreaker
|
9
|
-
|
10
9
|
# Default circuit breaker configuration. Can be overridden
|
11
10
|
circuit_handler do |handler|
|
12
11
|
handler.failure_threshold = 5
|
@@ -15,33 +14,49 @@ module Px::Service::Client
|
|
15
14
|
handler.excluded_exceptions = [Px::Service::ServiceRequestError]
|
16
15
|
end
|
17
16
|
|
18
|
-
|
19
|
-
|
17
|
+
cattr_accessor :circuit_state do
|
18
|
+
::CircuitBreaker::CircuitState.new
|
20
19
|
end
|
21
|
-
end
|
22
20
|
|
21
|
+
alias_method_chain :make_request, :breaker
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue StandardError => ex
|
39
|
-
# Wrap other exceptions, includes CircuitBreaker::CircuitBrokenException
|
40
|
-
raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
|
41
|
-
end
|
24
|
+
##
|
25
|
+
# Make the request, respecting the circuit breaker, if configured
|
26
|
+
def make_request_with_breaker(method, uri, query: nil, headers: nil, body: nil)
|
27
|
+
Future.new do
|
28
|
+
state = self.class.circuit_state
|
29
|
+
handler = self.class.circuit_handler
|
30
|
+
|
31
|
+
if handler.is_tripped(state)
|
32
|
+
handler.logger.debug("handle: breaker is tripped, refusing to execute: #{state}") if handler.logger
|
33
|
+
begin
|
34
|
+
handler.on_circuit_open(state)
|
35
|
+
rescue StandardError => ex
|
36
|
+
# Wrap and reroute other exceptions, includes CircuitBreaker::CircuitBrokenException
|
37
|
+
raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
|
42
38
|
end
|
43
39
|
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
response = make_request_without_breaker(
|
43
|
+
method,
|
44
|
+
uri,
|
45
|
+
query: query,
|
46
|
+
headers: headers,
|
47
|
+
body: body,
|
48
|
+
timeout: handler.invocation_timeout)
|
49
|
+
|
50
|
+
result = response.value!
|
51
|
+
handler.on_success(state)
|
52
|
+
|
53
|
+
result
|
54
|
+
rescue Px::Service::ServiceError
|
55
|
+
handler.on_failure(state)
|
56
|
+
raise
|
57
|
+
end
|
44
58
|
end
|
45
59
|
end
|
60
|
+
|
46
61
|
end
|
47
62
|
end
|
@@ -56,6 +56,17 @@ module Px::Service::Client
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
def value!
|
60
|
+
if @completed
|
61
|
+
result = @value
|
62
|
+
else
|
63
|
+
result = wait_for_value(nil)
|
64
|
+
end
|
65
|
+
raise result if result.kind_of?(Exception)
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
59
70
|
def completed?
|
60
71
|
@completed
|
61
72
|
end
|
data/px-service-client.gemspec
CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
27
27
|
spec.add_development_dependency "rake"
|
28
28
|
spec.add_development_dependency "webmock"
|
29
|
+
spec.add_development_dependency "pry"
|
29
30
|
spec.add_development_dependency "pry-byebug"
|
30
31
|
spec.add_development_dependency "vcr"
|
31
32
|
spec.add_development_dependency "guard"
|
@@ -1,11 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Px::Service::Client::CircuitBreaker do
|
4
|
-
|
5
4
|
let(:subject_class) {
|
6
|
-
Class.new
|
5
|
+
Class.new do
|
6
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
7
|
+
Px::Service::Client::Future.new do
|
8
|
+
_result
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end.tap do |c|
|
7
12
|
# Anonymous classes don't have a name. Stub out :name so that things work
|
8
13
|
allow(c).to receive(:name).and_return("CircuitBreaker")
|
14
|
+
c.include(Px::Service::Client::CircuitBreaker)
|
9
15
|
end
|
10
16
|
}
|
11
17
|
|
@@ -29,85 +35,171 @@ describe Px::Service::Client::CircuitBreaker do
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
32
|
-
describe '#
|
33
|
-
context "when the
|
38
|
+
describe '#make_request' do
|
39
|
+
context "when the underlying request method succeeds" do
|
34
40
|
before :each do
|
35
|
-
subject_class.send(:define_method, :
|
36
|
-
"returned
|
41
|
+
subject_class.send(:define_method, :_result) do
|
42
|
+
"returned test"
|
37
43
|
end
|
44
|
+
end
|
38
45
|
|
39
|
-
|
46
|
+
it "returns a Future" do
|
47
|
+
expect(subject.make_request(:get, "http://test")).to be_a_kind_of(Px::Service::Client::Future)
|
40
48
|
end
|
41
49
|
|
42
50
|
it "returns the return value" do
|
43
|
-
expect(subject.
|
51
|
+
expect(subject.make_request(:get, "http://test").value!).to eq("returned test")
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when the breaker is open" do
|
55
|
+
before :each do
|
56
|
+
allow(subject_class.circuit_handler).to receive(:is_timeout_exceeded).and_return(true)
|
57
|
+
|
58
|
+
subject.circuit_state.trip
|
59
|
+
subject.circuit_state.last_failure_time = Time.now
|
60
|
+
subject.circuit_state.failure_count = 5
|
61
|
+
end
|
62
|
+
|
63
|
+
it "resets the failure count of the breaker" do
|
64
|
+
expect {
|
65
|
+
subject.make_request(:get, "http://test").value!
|
66
|
+
}.to change{subject.class.circuit_state.failure_count}.to(0)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "closes the breaker" do
|
70
|
+
expect {
|
71
|
+
subject.make_request(:get, "http://test").value!
|
72
|
+
}.to change{subject.class.circuit_state.closed?}.from(false).to(true)
|
73
|
+
end
|
44
74
|
end
|
45
75
|
end
|
46
76
|
|
47
77
|
context "when the wrapped method fails with a ServiceRequestError" do
|
48
78
|
before :each do
|
49
|
-
subject_class.send(:define_method, :
|
79
|
+
subject_class.send(:define_method, :_result) do
|
50
80
|
raise Px::Service::ServiceRequestError.new("Error", 404)
|
51
81
|
end
|
52
|
-
|
53
|
-
subject_class.circuit_method(:test_method)
|
54
82
|
end
|
55
83
|
|
56
84
|
it "raises a ServiceRequestError" do
|
57
|
-
expect{
|
58
|
-
subject.
|
85
|
+
expect {
|
86
|
+
subject.make_request(:get, "http://test").value!
|
59
87
|
}.to raise_error(Px::Service::ServiceRequestError, "Error")
|
60
88
|
end
|
89
|
+
|
90
|
+
it "does not increment the failure count of the breaker" do
|
91
|
+
expect {
|
92
|
+
subject.make_request(:get, "http://test").value! rescue nil
|
93
|
+
}.not_to change{subject.class.circuit_state.failure_count}
|
94
|
+
end
|
61
95
|
end
|
62
96
|
|
63
97
|
context "when the wrapped method fails with a ServiceError" do
|
64
98
|
before :each do
|
65
|
-
subject_class.send(:define_method, :
|
99
|
+
subject_class.send(:define_method, :_result) do
|
66
100
|
raise Px::Service::ServiceError.new("Error", 500)
|
67
101
|
end
|
68
|
-
|
69
|
-
subject_class.circuit_method(:test_method)
|
70
102
|
end
|
71
103
|
|
72
104
|
it "raises a ServiceError" do
|
73
|
-
expect{
|
74
|
-
subject.
|
105
|
+
expect {
|
106
|
+
subject.make_request(:get, "http://test").value!
|
75
107
|
}.to raise_error(Px::Service::ServiceError, "Error")
|
76
108
|
end
|
109
|
+
|
110
|
+
it "increments the failure count of the breaker" do
|
111
|
+
expect {
|
112
|
+
subject.make_request(:get, "http://test").value! rescue nil
|
113
|
+
}.to change{subject.class.circuit_state.failure_count}.by(1)
|
114
|
+
end
|
77
115
|
end
|
78
116
|
|
79
|
-
context "when the
|
117
|
+
context "when the circuit is open" do
|
80
118
|
before :each do
|
81
|
-
subject_class.send(:define_method, :
|
82
|
-
|
119
|
+
subject_class.send(:define_method, :_result) do
|
120
|
+
"should not be called"
|
83
121
|
end
|
84
122
|
|
85
|
-
|
123
|
+
subject.circuit_state.trip
|
124
|
+
subject.circuit_state.last_failure_time = Time.now
|
86
125
|
end
|
87
126
|
|
88
127
|
it "raises a ServiceError" do
|
89
|
-
expect{
|
90
|
-
subject.
|
128
|
+
expect {
|
129
|
+
subject.make_request(:get, "http://test").value!
|
91
130
|
}.to raise_error(Px::Service::ServiceError)
|
92
131
|
end
|
93
132
|
end
|
94
133
|
|
95
|
-
context "
|
134
|
+
context "with multiple classes" do
|
135
|
+
let(:other_class) {
|
136
|
+
Class.new do
|
137
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
138
|
+
Px::Service::Client::Future.new do
|
139
|
+
"result"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end.tap do |c|
|
143
|
+
# Anonymous classes don't have a name. Stub out :name so that things work
|
144
|
+
allow(c).to receive(:name).and_return("OtherCircuitBreaker")
|
145
|
+
c.include(Px::Service::Client::CircuitBreaker)
|
146
|
+
end
|
147
|
+
}
|
148
|
+
|
149
|
+
let(:other) { other_class.new }
|
150
|
+
|
96
151
|
before :each do
|
97
|
-
subject_class.send(:define_method, :
|
152
|
+
subject_class.send(:define_method, :_result) do
|
98
153
|
"should not be called"
|
99
154
|
end
|
155
|
+
end
|
100
156
|
|
101
|
-
|
157
|
+
context "when the breaker opens on the first instance" do
|
158
|
+
before :each do
|
159
|
+
subject.circuit_state.trip
|
160
|
+
subject.circuit_state.last_failure_time = Time.now
|
161
|
+
end
|
102
162
|
|
103
|
-
|
163
|
+
it "raises a ServiceError on the first instance" do
|
164
|
+
expect {
|
165
|
+
subject.make_request(:get, "http://test").value!
|
166
|
+
}.to raise_error(Px::Service::ServiceError)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "does not raise a ServiceError on the second instance" do
|
170
|
+
expect(other.make_request(:get, "http://test").value!).to eq("result")
|
171
|
+
end
|
104
172
|
end
|
173
|
+
end
|
105
174
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
175
|
+
context "with multiple instances of the same class" do
|
176
|
+
let(:other) { subject_class.new }
|
177
|
+
|
178
|
+
before :each do
|
179
|
+
subject_class.send(:define_method, :_result) do
|
180
|
+
"should not be called"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when the breaker opens on the first instance" do
|
185
|
+
before :each do
|
186
|
+
subject.circuit_state.trip
|
187
|
+
subject.circuit_state.last_failure_time = Time.now
|
188
|
+
end
|
189
|
+
|
190
|
+
it "raises a ServiceError on the first instance" do
|
191
|
+
expect {
|
192
|
+
subject.make_request(:get, "http://test").value!
|
193
|
+
}.to raise_error(Px::Service::ServiceError)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "raises a ServiceError on the second instance" do
|
197
|
+
expect {
|
198
|
+
other.make_request(:get, "http://test").value!
|
199
|
+
}.to raise_error(Px::Service::ServiceError)
|
200
|
+
end
|
110
201
|
end
|
111
202
|
end
|
203
|
+
|
112
204
|
end
|
113
205
|
end
|
@@ -46,7 +46,7 @@ describe Px::Service::Client::Future do
|
|
46
46
|
it "does not call the method on the value" do
|
47
47
|
called = false
|
48
48
|
Fiber.new do
|
49
|
-
subject.
|
49
|
+
subject.value
|
50
50
|
called = true
|
51
51
|
end.resume
|
52
52
|
|
@@ -86,6 +86,53 @@ describe Px::Service::Client::Future do
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
+
describe '#value!' do
|
90
|
+
context "when the future is not complete" do
|
91
|
+
it "does not call the method on the value" do
|
92
|
+
called = false
|
93
|
+
Fiber.new do
|
94
|
+
subject.value!
|
95
|
+
called = true
|
96
|
+
end.resume
|
97
|
+
|
98
|
+
expect(called).to eq(false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "when the future is already complete" do
|
103
|
+
it "returns the value" do
|
104
|
+
subject.complete(value)
|
105
|
+
expect(subject.value!).to eq(value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when the value is an exception" do
|
110
|
+
it "returns the exception" do
|
111
|
+
Fiber.new do
|
112
|
+
expect {
|
113
|
+
subject.value!
|
114
|
+
}.to raise_error(ArgumentError)
|
115
|
+
end.resume
|
116
|
+
|
117
|
+
Fiber.new do
|
118
|
+
subject.complete(ArgumentError.new("Error"))
|
119
|
+
end.resume
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "when the method returns a value" do
|
124
|
+
it "returns the value" do
|
125
|
+
Fiber.new do
|
126
|
+
expect(subject.value!).to eq(value)
|
127
|
+
end.resume
|
128
|
+
|
129
|
+
Fiber.new do
|
130
|
+
subject.complete(value)
|
131
|
+
end.resume
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
89
136
|
describe '#method_missing' do
|
90
137
|
context "when the future is already complete" do
|
91
138
|
context "when the method raised an exception" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: px-service-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Micacchi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: will_paginate
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: pry-byebug
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -269,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
269
283
|
version: '0'
|
270
284
|
requirements: []
|
271
285
|
rubyforge_project:
|
272
|
-
rubygems_version: 2.
|
286
|
+
rubygems_version: 2.4.5
|
273
287
|
signing_key:
|
274
288
|
specification_version: 4
|
275
289
|
summary: Common service client behaviours for Ruby applications
|