cdc-solid-queue 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +103 -0
- data/lib/cdc/solid_queue/cli.rb +21 -0
- data/lib/cdc/solid_queue/configuration.rb +18 -1
- data/lib/cdc/solid_queue/downstream_processor.rb +65 -0
- data/lib/cdc/solid_queue/processor_job.rb +6 -1
- data/lib/cdc/solid_queue/railtie.rb +6 -1
- data/lib/cdc/solid_queue/tasks/cdc_solid_queue.rake +1 -5
- data/lib/cdc/solid_queue/version.rb +1 -1
- data/lib/cdc/solid_queue.rb +2 -0
- data/sig/cdc/solid_queue.rbs +39 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a31a7915a2b6ba29afe3497045f7be40477b0388b89a0357a18bb77428f5701c
|
|
4
|
+
data.tar.gz: 4485d2cdf5c8137dc7a11503364c3f05a1d00691d83d0be18f3c5a194ba6c968
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f3a59fef64e5227fcf6ddab9ce45363221058baa8dbed1932e7eedd75620fe2f1b4f0382c97232ad6cfc4f15ac30044f3427e05c21aebec2b4283aafb255f27
|
|
7
|
+
data.tar.gz: 6aacc4886cd318f2ce12dfcb6164d3c059c8a37064ef0daced35dc016d40a4047e2054e11b4f0a6d84a3af01447b060452f9e1c6e3dae5f1f187c53df1532035
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
- Optional downstream processor delegation to `cdc-concurrent` and `cdc-parallel`.
|
|
8
|
+
- Rails example now demonstrates `cdc-concurrent` downstream processing.
|
|
9
|
+
- Benchmark can measure direct downstream delegation overhead.
|
|
10
|
+
|
|
11
|
+
## 0.1.2
|
|
12
|
+
|
|
13
|
+
- Minimal Rails app example.
|
|
14
|
+
- Local smoke tests.
|
|
15
|
+
- Enqueue overhead benchmark.
|
|
16
|
+
|
|
3
17
|
## 0.1.1
|
|
4
18
|
|
|
5
19
|
- Initial implementation skeleton.
|
data/README.md
CHANGED
|
@@ -54,6 +54,44 @@ class supports it. When `preserve_order` is enabled, the enqueued payload also
|
|
|
54
54
|
includes cdc-solid-queue metadata with the configured ordering key and computed
|
|
55
55
|
ordering value.
|
|
56
56
|
|
|
57
|
+
## Downstream Processing
|
|
58
|
+
|
|
59
|
+
Processor jobs can delegate work to CDC downstream runtime primitives. The
|
|
60
|
+
default downstream runtime is `:concurrent`, backed by `cdc-concurrent`, which
|
|
61
|
+
fits Solid Queue jobs that spend most of their time on I/O. CPU-heavy work can
|
|
62
|
+
opt into `:parallel`, backed by `cdc-parallel`, in Ruby 4 applications.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class WebhookProcessor < CDC::Core::Processor
|
|
66
|
+
concurrent_safe!
|
|
67
|
+
|
|
68
|
+
def process(event)
|
|
69
|
+
# perform I/O-bound work
|
|
70
|
+
CDC::Core::ProcessorResult.success(event)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
CDC::SolidQueue.configure do |config|
|
|
75
|
+
config.processor_job = UserChangedJob
|
|
76
|
+
config.downstream_processor = WebhookProcessor.new
|
|
77
|
+
config.downstream_runtime = :concurrent
|
|
78
|
+
config.downstream_options = { concurrency: 100, timeout: 5.0 }
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use `:parallel` only when the processor is Ractor-safe and the application runs
|
|
83
|
+
on Ruby 4:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
config.downstream_runtime = :parallel
|
|
87
|
+
config.downstream_options = { size: 4, timeout: 5 }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Both runtime gems are optional. Add `cdc-concurrent` or `cdc-parallel` to the
|
|
91
|
+
application Gemfile when selecting that runtime. Without a configured
|
|
92
|
+
`downstream_processor`, `CDC::SolidQueue::ProcessorJob` falls back to the job's
|
|
93
|
+
own `#process(event)` method.
|
|
94
|
+
|
|
57
95
|
## Rails Task
|
|
58
96
|
|
|
59
97
|
Rails applications can load the Railtie integration:
|
|
@@ -72,6 +110,71 @@ The task wires `Pgoutput::Client::Runner`, `Pgoutput::RelationTracker`,
|
|
|
72
110
|
`Pgoutput::Decoder`, and `Pgoutput::SourceAdapter::Cdc` into the
|
|
73
111
|
`CDC::SolidQueue::Runner`.
|
|
74
112
|
|
|
113
|
+
See `examples/rails_app` for a minimal Rails-side setup with a Solid Queue job,
|
|
114
|
+
initializer, Railtie require, and a local PostgreSQL container configured for
|
|
115
|
+
logical replication.
|
|
116
|
+
|
|
117
|
+
## Smoke Tests
|
|
118
|
+
|
|
119
|
+
Run local smoke tests without PostgreSQL or Rails:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
bundle exec rake smoke:local
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The smoke tests verify enqueue metadata, event rehydration, and
|
|
126
|
+
checkpoint-after-enqueue behavior.
|
|
127
|
+
|
|
128
|
+
## Benchmark
|
|
129
|
+
|
|
130
|
+
Run the enqueue overhead benchmark:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bundle exec rake benchmark:enqueue
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Set `CDC_SOLID_QUEUE_BENCH_EVENTS` to control the event count.
|
|
137
|
+
Set `CDC_SOLID_QUEUE_BENCH_MODE=downstream_direct` to measure direct downstream
|
|
138
|
+
processor delegation overhead without Solid Queue enqueue translation.
|
|
139
|
+
|
|
140
|
+
Example local result on Ruby 3.4.9:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
events=1000000 elapsed=15.7210s rate=63609.14 events/s
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This is an upper-bound microbenchmark for the Ruby-side enqueue translation
|
|
147
|
+
layer. It measures event serialization, queue and ordering metadata calculation,
|
|
148
|
+
and dispatch to a fake benchmark job. It does not measure real Solid Queue
|
|
149
|
+
database inserts, Rails job execution, PostgreSQL replication, pgoutput
|
|
150
|
+
decoding, network I/O, or checkpoint persistence.
|
|
151
|
+
|
|
152
|
+
In that run, `cdc-solid-queue` translated and dispatched about 63.6k synthetic
|
|
153
|
+
events per second, so real throughput will usually be dominated by Solid Queue
|
|
154
|
+
persistence, database latency, job execution cost, and CDC source throughput.
|
|
155
|
+
|
|
156
|
+
Example `downstream_direct` results on the same machine:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
mode=downstream_direct events=100000000 elapsed=16.2669s rate=6147457.32 events/s
|
|
160
|
+
mode=downstream_direct events=1000000000 elapsed=157.8708s rate=6334292.58 events/s
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
These runs measure the lowest-overhead downstream delegation path:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
CDC::SolidQueue::DownstreamProcessor
|
|
167
|
+
-> :direct runtime branch
|
|
168
|
+
-> BenchmarkProcessor#process(event)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
They do not measure Solid Queue enqueueing, Active Job serialization,
|
|
172
|
+
PostgreSQL CDC, pgoutput parsing or decoding, `cdc-concurrent`,
|
|
173
|
+
`cdc-parallel`, real application processor work, network I/O, or database I/O.
|
|
174
|
+
The result means the direct downstream adapter can dispatch about 6.1M to 6.3M
|
|
175
|
+
prebuilt synthetic events per second on that machine, making the adapter layer
|
|
176
|
+
negligible compared with real persistence, CDC source, and processor costs.
|
|
177
|
+
|
|
75
178
|
## MVP Checkpoint Rule
|
|
76
179
|
|
|
77
180
|
A checkpoint advances after the Solid Queue job is durably inserted. Job execution success is handled by Solid Queue retry semantics.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Command helpers used by Rails tasks and executable entrypoints.
|
|
6
|
+
module CLI
|
|
7
|
+
class << self
|
|
8
|
+
# Start PostgreSQL CDC ingestion using the global configuration.
|
|
9
|
+
#
|
|
10
|
+
# @return [Integer] number of enqueued events when the stream exits
|
|
11
|
+
def start
|
|
12
|
+
configuration = CDC::SolidQueue.configuration
|
|
13
|
+
enqueuer = CDC::SolidQueue::Enqueuer.new(configuration)
|
|
14
|
+
stream = CDC::SolidQueue::PostgresqlStream.new(configuration)
|
|
15
|
+
|
|
16
|
+
CDC::SolidQueue::Runner.new(stream:, enqueuer:).start
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -12,8 +12,11 @@ module CDC
|
|
|
12
12
|
SUPPORTED_SOURCE = :postgresql
|
|
13
13
|
# Supported ordering scopes for serialized CDC events.
|
|
14
14
|
ORDERING_KEYS = %i[identity primary_key relation transaction global none].freeze
|
|
15
|
+
# Supported downstream execution runtimes for processor jobs.
|
|
16
|
+
DOWNSTREAM_RUNTIMES = %i[concurrent parallel direct].freeze
|
|
15
17
|
|
|
16
|
-
attr_accessor :processor_job, :queue, :preserve_order, :ordering_key, :postgresql, :checkpoint
|
|
18
|
+
attr_accessor :processor_job, :queue, :preserve_order, :ordering_key, :postgresql, :checkpoint,
|
|
19
|
+
:downstream_processor, :downstream_runtime, :downstream_options
|
|
17
20
|
|
|
18
21
|
# Build a configuration with safe defaults.
|
|
19
22
|
def initialize
|
|
@@ -23,6 +26,9 @@ module CDC
|
|
|
23
26
|
@ordering_key = :identity
|
|
24
27
|
@postgresql = {}
|
|
25
28
|
@checkpoint = Checkpoint.new
|
|
29
|
+
@downstream_processor = nil
|
|
30
|
+
@downstream_runtime = :concurrent
|
|
31
|
+
@downstream_options = {}
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
# Validate this configuration.
|
|
@@ -37,6 +43,7 @@ module CDC
|
|
|
37
43
|
validate_ordering_key!
|
|
38
44
|
validate_postgresql!
|
|
39
45
|
validate_checkpoint!
|
|
46
|
+
validate_downstream!
|
|
40
47
|
true
|
|
41
48
|
end
|
|
42
49
|
# rubocop:enable Naming/PredicateMethod
|
|
@@ -81,6 +88,16 @@ module CDC
|
|
|
81
88
|
|
|
82
89
|
raise ConfigurationError, 'checkpoint must respond to advance'
|
|
83
90
|
end
|
|
91
|
+
|
|
92
|
+
def validate_downstream!
|
|
93
|
+
unless DOWNSTREAM_RUNTIMES.include?(@downstream_runtime)
|
|
94
|
+
raise ConfigurationError, "downstream_runtime must be one of: #{DOWNSTREAM_RUNTIMES.join(', ')}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return if @downstream_processor.nil? || @downstream_processor.respond_to?(:process)
|
|
98
|
+
|
|
99
|
+
raise ConfigurationError, 'downstream_processor must respond to process'
|
|
100
|
+
end
|
|
84
101
|
end
|
|
85
102
|
end
|
|
86
103
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CDC
|
|
4
|
+
module SolidQueue
|
|
5
|
+
# Delegates processor-job work to CDC downstream runtime primitives.
|
|
6
|
+
class DownstreamProcessor
|
|
7
|
+
# @return [Configuration]
|
|
8
|
+
attr_reader :configuration
|
|
9
|
+
|
|
10
|
+
# @param configuration [Configuration]
|
|
11
|
+
def initialize(configuration)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Process one normalized CDC work item.
|
|
16
|
+
#
|
|
17
|
+
# @param item [Object]
|
|
18
|
+
# @return [Object]
|
|
19
|
+
def process(item)
|
|
20
|
+
case configuration.downstream_runtime
|
|
21
|
+
when :direct
|
|
22
|
+
processor.process(item)
|
|
23
|
+
when :concurrent
|
|
24
|
+
process_with_runtime(concurrent_runtime, item)
|
|
25
|
+
when :parallel
|
|
26
|
+
process_with_runtime(parallel_runtime, item)
|
|
27
|
+
else
|
|
28
|
+
raise ConfigurationError, "unsupported downstream_runtime: #{configuration.downstream_runtime.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def processor
|
|
35
|
+
configuration.downstream_processor || raise(ConfigurationError, 'downstream_processor is required')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process_with_runtime(runtime, item)
|
|
39
|
+
runtime.process(item)
|
|
40
|
+
ensure
|
|
41
|
+
runtime.shutdown
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def concurrent_runtime
|
|
45
|
+
require_runtime('cdc/concurrent', 'cdc-concurrent') unless defined?(CDC::Concurrent::Runtime)
|
|
46
|
+
CDC::Concurrent::Runtime.new(processor:, **configuration.downstream_options)
|
|
47
|
+
rescue LoadError => e
|
|
48
|
+
raise ConfigurationError, "cdc-concurrent is required for downstream_runtime :concurrent: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parallel_runtime
|
|
52
|
+
require_runtime('cdc/parallel', 'cdc-parallel') unless defined?(CDC::Parallel::Runtime)
|
|
53
|
+
CDC::Parallel::Runtime.new(processor:, **configuration.downstream_options)
|
|
54
|
+
rescue LoadError => e
|
|
55
|
+
raise ConfigurationError, "cdc-parallel is required for downstream_runtime :parallel: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def require_runtime(feature, gem_name)
|
|
59
|
+
require feature
|
|
60
|
+
rescue LoadError
|
|
61
|
+
raise LoadError, "install #{gem_name} and require #{feature}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -20,7 +20,12 @@ module CDC
|
|
|
20
20
|
# @param payload [Hash]
|
|
21
21
|
# @return [Object] process return value
|
|
22
22
|
def perform(payload)
|
|
23
|
-
|
|
23
|
+
event = EventSerializer.load_event(payload)
|
|
24
|
+
if SolidQueue.configuration.downstream_processor
|
|
25
|
+
return DownstreamProcessor.new(SolidQueue.configuration).process(event)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
process(event)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# Process a normalized CDC event payload.
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
namespace :cdc_solid_queue do
|
|
4
4
|
desc 'Start PostgreSQL CDC ingestion into Solid Queue'
|
|
5
5
|
task start: :environment do
|
|
6
|
-
|
|
7
|
-
enqueuer = CDC::SolidQueue::Enqueuer.new(configuration)
|
|
8
|
-
stream = CDC::SolidQueue::PostgresqlStream.new(configuration)
|
|
9
|
-
|
|
10
|
-
CDC::SolidQueue::Runner.new(stream:, enqueuer:).start
|
|
6
|
+
CDC::SolidQueue::CLI.start
|
|
11
7
|
end
|
|
12
8
|
end
|
data/lib/cdc/solid_queue.rb
CHANGED
|
@@ -6,9 +6,11 @@ require_relative 'solid_queue/event_serializer'
|
|
|
6
6
|
require_relative 'solid_queue/checkpoint'
|
|
7
7
|
require_relative 'solid_queue/configuration'
|
|
8
8
|
require_relative 'solid_queue/enqueuer'
|
|
9
|
+
require_relative 'solid_queue/downstream_processor'
|
|
9
10
|
require_relative 'solid_queue/processor_job'
|
|
10
11
|
require_relative 'solid_queue/postgresql_stream'
|
|
11
12
|
require_relative 'solid_queue/runner'
|
|
13
|
+
require_relative 'solid_queue/cli'
|
|
12
14
|
|
|
13
15
|
# Namespace for Change Data Capture integrations.
|
|
14
16
|
module CDC
|
data/sig/cdc/solid_queue.rbs
CHANGED
|
@@ -32,6 +32,22 @@ module Pgoutput
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
module CDC
|
|
35
|
+
module Concurrent
|
|
36
|
+
class Runtime
|
|
37
|
+
def initialize: (processor: untyped, **untyped options) -> void
|
|
38
|
+
def process: (untyped item) -> untyped
|
|
39
|
+
def shutdown: () -> untyped
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module Parallel
|
|
44
|
+
class Runtime
|
|
45
|
+
def initialize: (processor: untyped, **untyped options) -> void
|
|
46
|
+
def process: (untyped item) -> untyped
|
|
47
|
+
def shutdown: () -> untyped
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
module SolidQueue
|
|
36
52
|
VERSION: String
|
|
37
53
|
|
|
@@ -54,6 +70,7 @@ module CDC
|
|
|
54
70
|
class Configuration
|
|
55
71
|
SUPPORTED_SOURCE: Symbol
|
|
56
72
|
ORDERING_KEYS: Array[Symbol]
|
|
73
|
+
DOWNSTREAM_RUNTIMES: Array[Symbol]
|
|
57
74
|
|
|
58
75
|
attr_accessor processor_job: untyped
|
|
59
76
|
attr_accessor queue: String
|
|
@@ -61,6 +78,9 @@ module CDC
|
|
|
61
78
|
attr_accessor ordering_key: Symbol
|
|
62
79
|
attr_accessor postgresql: Hash[Symbol, untyped]
|
|
63
80
|
attr_accessor checkpoint: untyped
|
|
81
|
+
attr_accessor downstream_processor: untyped
|
|
82
|
+
attr_accessor downstream_runtime: Symbol
|
|
83
|
+
attr_accessor downstream_options: Hash[Symbol, untyped]
|
|
64
84
|
|
|
65
85
|
def initialize: () -> void
|
|
66
86
|
def validate!: () -> true
|
|
@@ -73,6 +93,7 @@ module CDC
|
|
|
73
93
|
def validate_ordering_key!: () -> nil
|
|
74
94
|
def validate_postgresql!: () -> nil
|
|
75
95
|
def validate_checkpoint!: () -> nil
|
|
96
|
+
def validate_downstream!: () -> nil
|
|
76
97
|
end
|
|
77
98
|
|
|
78
99
|
class EventSerializer
|
|
@@ -116,6 +137,20 @@ module CDC
|
|
|
116
137
|
def ordering_value: (Hash[untyped, untyped] payload) -> untyped
|
|
117
138
|
end
|
|
118
139
|
|
|
140
|
+
class DownstreamProcessor
|
|
141
|
+
attr_reader configuration: Configuration
|
|
142
|
+
def initialize: (Configuration configuration) -> void
|
|
143
|
+
def process: (untyped item) -> untyped
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def processor: () -> untyped
|
|
148
|
+
def process_with_runtime: (untyped runtime, untyped item) -> untyped
|
|
149
|
+
def concurrent_runtime: () -> untyped
|
|
150
|
+
def parallel_runtime: () -> untyped
|
|
151
|
+
def require_runtime: (String feature, String gem_name) -> untyped
|
|
152
|
+
end
|
|
153
|
+
|
|
119
154
|
module ProcessorJob
|
|
120
155
|
def self.included: (untyped base) -> void
|
|
121
156
|
def perform: (Hash[untyped, untyped] payload) -> untyped
|
|
@@ -152,6 +187,10 @@ module CDC
|
|
|
152
187
|
def metadata_value: (untyped metadata, Symbol name) -> untyped
|
|
153
188
|
end
|
|
154
189
|
|
|
190
|
+
module CLI
|
|
191
|
+
def self.start: () -> Integer
|
|
192
|
+
end
|
|
193
|
+
|
|
155
194
|
class Railtie < ::Rails::Railtie
|
|
156
195
|
end
|
|
157
196
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cdc-solid-queue
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -91,7 +91,9 @@ files:
|
|
|
91
91
|
- README.md
|
|
92
92
|
- lib/cdc/solid_queue.rb
|
|
93
93
|
- lib/cdc/solid_queue/checkpoint.rb
|
|
94
|
+
- lib/cdc/solid_queue/cli.rb
|
|
94
95
|
- lib/cdc/solid_queue/configuration.rb
|
|
96
|
+
- lib/cdc/solid_queue/downstream_processor.rb
|
|
95
97
|
- lib/cdc/solid_queue/enqueuer.rb
|
|
96
98
|
- lib/cdc/solid_queue/error.rb
|
|
97
99
|
- lib/cdc/solid_queue/event_serializer.rb
|