pub_sub_model_sync 0.5.8.2 → 1.0.beta
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/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +44 -0
- data/Dockerfile +6 -0
- data/Gemfile.lock +144 -135
- data/README.md +370 -194
- data/docker-compose.yaml +12 -0
- data/docs/notifications-diagram.png +0 -0
- data/lib/pub_sub_model_sync.rb +2 -0
- data/lib/pub_sub_model_sync/base.rb +22 -5
- data/lib/pub_sub_model_sync/config.rb +15 -7
- data/lib/pub_sub_model_sync/message_processor.rb +15 -10
- data/lib/pub_sub_model_sync/message_publisher.rb +92 -20
- data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
- data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
- data/lib/pub_sub_model_sync/payload.rb +32 -16
- data/lib/pub_sub_model_sync/publisher.rb +43 -21
- data/lib/pub_sub_model_sync/publisher_concern.rb +54 -44
- data/lib/pub_sub_model_sync/run_subscriber.rb +104 -0
- data/lib/pub_sub_model_sync/service_base.rb +47 -13
- data/lib/pub_sub_model_sync/service_google.rb +53 -17
- data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
- data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
- data/lib/pub_sub_model_sync/subscriber.rb +14 -61
- data/lib/pub_sub_model_sync/subscriber_concern.rb +21 -28
- data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
- data/lib/pub_sub_model_sync/transaction.rb +57 -0
- data/lib/pub_sub_model_sync/version.rb +1 -1
- metadata +9 -4
@@ -7,14 +7,20 @@ end
|
|
7
7
|
|
8
8
|
module PubSubModelSync
|
9
9
|
class ServiceKafka < ServiceBase
|
10
|
+
QTY_WORKERS = 10
|
11
|
+
LISTEN_SETTINGS = {}.freeze
|
12
|
+
PUBLISH_SETTINGS = {}.freeze
|
13
|
+
PRODUCER_SETTINGS = { delivery_threshold: 200, delivery_interval: 30 }.freeze
|
10
14
|
cattr_accessor :producer
|
11
|
-
|
15
|
+
|
16
|
+
# @!attribute topic_names (Array): ['topic 1', 'topic 2']
|
17
|
+
attr_accessor :service, :consumer, :topic_names
|
12
18
|
|
13
19
|
def initialize
|
14
|
-
@config = PubSubModelSync::Config
|
15
20
|
settings = config.kafka_connection
|
16
21
|
settings[1][:client_id] ||= config.subscription_key
|
17
22
|
@service = Kafka.new(*settings)
|
23
|
+
@topic_names = ensure_topics(Array(config.topic_name || 'model_sync'))
|
18
24
|
end
|
19
25
|
|
20
26
|
def listen_messages
|
@@ -28,12 +34,10 @@ module PubSubModelSync
|
|
28
34
|
end
|
29
35
|
|
30
36
|
def publish(payload)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
producer.produce(payload.to_json, settings)
|
36
|
-
producer.deliver_messages
|
37
|
+
message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
|
38
|
+
message_topics.each do |topic_name|
|
39
|
+
producer.produce(encode_payload(payload), message_settings(payload, topic_name))
|
40
|
+
end
|
37
41
|
end
|
38
42
|
|
39
43
|
def stop
|
@@ -43,22 +47,45 @@ module PubSubModelSync
|
|
43
47
|
|
44
48
|
private
|
45
49
|
|
50
|
+
def message_settings(payload, topic_name)
|
51
|
+
{
|
52
|
+
topic: ensure_topics(topic_name),
|
53
|
+
partition_key: payload.headers[:ordering_key],
|
54
|
+
headers: { SERVICE_KEY => true }
|
55
|
+
}.merge(PUBLISH_SETTINGS)
|
56
|
+
end
|
57
|
+
|
46
58
|
def start_consumer
|
47
|
-
|
48
|
-
consumer.
|
59
|
+
subscription_key = config.subscription_key
|
60
|
+
@consumer = service.consumer(group_id: subscription_key)
|
61
|
+
topic_names.each do |topic_name|
|
62
|
+
log("Subscribed to topic: #{topic_name} as #{subscription_key}")
|
63
|
+
consumer.subscribe(topic_name)
|
64
|
+
end
|
49
65
|
end
|
50
66
|
|
51
67
|
def producer
|
52
68
|
return self.class.producer if self.class.producer
|
53
69
|
|
54
70
|
at_exit { self.class.producer.shutdown }
|
55
|
-
self.class.producer = service.
|
71
|
+
self.class.producer = service.async_producer(PRODUCER_SETTINGS)
|
56
72
|
end
|
57
73
|
|
58
74
|
def process_message(message)
|
59
|
-
|
75
|
+
super(message.value) if message.headers[SERVICE_KEY]
|
76
|
+
end
|
60
77
|
|
61
|
-
|
78
|
+
# Check topic existence, create if missing topic
|
79
|
+
# @param names (Array<String>|String)
|
80
|
+
# @return (Array|String) return @param names
|
81
|
+
def ensure_topics(names)
|
82
|
+
missing_topics = Array(names) - (@known_topics || service.topics)
|
83
|
+
missing_topics.each do |name|
|
84
|
+
service.create_topic(name)
|
85
|
+
end
|
86
|
+
@known_topics ||= [] # cache service.topics to reduce verification time
|
87
|
+
@known_topics = (@known_topics + Array(names)).uniq
|
88
|
+
names
|
62
89
|
end
|
63
90
|
end
|
64
91
|
end
|
@@ -7,18 +7,26 @@ end
|
|
7
7
|
|
8
8
|
module PubSubModelSync
|
9
9
|
class ServiceRabbit < ServiceBase
|
10
|
-
|
10
|
+
QUEUE_SETTINGS = { durable: true, auto_delete: false }.freeze
|
11
|
+
LISTEN_SETTINGS = { manual_ack: true }.freeze
|
12
|
+
PUBLISH_SETTINGS = {}.freeze
|
13
|
+
|
14
|
+
# @!attribute topic_names (Array): ['Topic 1', 'Topic 2']
|
15
|
+
# @!attribute channels (Array): [Channel1]
|
16
|
+
# @!attribute exchanges (Hash<key: Exchange>): {topic_name: Exchange1}
|
17
|
+
attr_accessor :service, :topic_names, :channels, :exchanges
|
11
18
|
|
12
19
|
def initialize
|
13
|
-
@config = PubSubModelSync::Config
|
14
20
|
@service = Bunny.new(*config.bunny_connection)
|
21
|
+
@topic_names = Array(config.topic_name || 'model_sync')
|
22
|
+
@channels = []
|
23
|
+
@exchanges = {}
|
15
24
|
end
|
16
25
|
|
17
26
|
def listen_messages
|
18
27
|
log('Listener starting...')
|
19
|
-
|
28
|
+
subscribe_to_queues { |queue| queue.subscribe(LISTEN_SETTINGS, &method(:process_message)) }
|
20
29
|
log('Listener started')
|
21
|
-
queue.subscribe(subscribe_settings, &method(:process_message))
|
22
30
|
loop { sleep 5 }
|
23
31
|
rescue PubSubModelSync::Runner::ShutDown
|
24
32
|
log('Listener stopped')
|
@@ -40,54 +48,54 @@ module PubSubModelSync
|
|
40
48
|
|
41
49
|
def stop
|
42
50
|
log('Listener stopping...')
|
43
|
-
|
51
|
+
channels.each(&:close)
|
44
52
|
service.close
|
45
53
|
end
|
46
54
|
|
47
55
|
private
|
48
56
|
|
49
|
-
def message_settings
|
57
|
+
def message_settings(payload)
|
50
58
|
{
|
51
|
-
routing_key:
|
59
|
+
routing_key: payload.headers[:ordering_key],
|
52
60
|
type: SERVICE_KEY,
|
53
61
|
persistent: true
|
54
62
|
}.merge(PUBLISH_SETTINGS)
|
55
63
|
end
|
56
64
|
|
57
|
-
def queue_settings
|
58
|
-
{ durable: true, auto_delete: false }
|
59
|
-
end
|
60
|
-
|
61
|
-
def subscribe_settings
|
62
|
-
{ manual_ack: false }.merge(LISTEN_SETTINGS)
|
63
|
-
end
|
64
|
-
|
65
65
|
def process_message(_delivery_info, meta_info, payload)
|
66
|
-
|
67
|
-
|
68
|
-
super(payload)
|
66
|
+
super(payload) if meta_info[:type] == SERVICE_KEY
|
69
67
|
end
|
70
68
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
def subscribe_to_queues(&block)
|
70
|
+
@channels = []
|
71
|
+
topic_names.each do |topic_name|
|
72
|
+
subscribe_to_exchange(topic_name) do |channel, exchange|
|
73
|
+
queue = channel.queue(config.subscription_key, QUEUE_SETTINGS)
|
74
|
+
queue.bind(exchange)
|
75
|
+
@channels << channel
|
76
|
+
log("Subscribed to topic: #{topic_name} as #{queue.name}")
|
77
|
+
block.call(queue)
|
78
|
+
end
|
79
|
+
end
|
76
80
|
end
|
77
81
|
|
78
|
-
def subscribe_to_exchange
|
79
|
-
|
80
|
-
|
82
|
+
def subscribe_to_exchange(topic_name, &block)
|
83
|
+
topic_name = topic_name.to_s
|
84
|
+
exchanges[topic_name] ||= begin
|
85
|
+
service.start
|
86
|
+
channel = service.create_channel
|
87
|
+
channel.fanout(topic_name)
|
88
|
+
end
|
89
|
+
block.call(channel, exchanges[topic_name])
|
81
90
|
end
|
82
91
|
|
83
92
|
def deliver_data(payload)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
service.close
|
93
|
+
message_topics = Array(payload.headers[:topic_name] || config.default_topic_name)
|
94
|
+
message_topics.each do |topic_name|
|
95
|
+
subscribe_to_exchange(topic_name) do |_channel, exchange|
|
96
|
+
exchange.publish(encode_payload(payload), message_settings(payload))
|
97
|
+
end
|
98
|
+
end
|
91
99
|
end
|
92
100
|
end
|
93
101
|
end
|
@@ -1,69 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PubSubModelSync
|
4
|
-
class Subscriber
|
5
|
-
attr_accessor :klass, :action, :
|
6
|
-
attr_reader :payload
|
7
|
-
|
8
|
-
# @param
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
class Subscriber < PubSubModelSync::Base
|
5
|
+
attr_accessor :klass, :action, :mapping, :settings, :from_klass, :mode
|
6
|
+
attr_reader :payload, :model
|
7
|
+
|
8
|
+
# @param klass (String) class name
|
9
|
+
# @param action (Symbol) @refer SubscriberConcern.ps_subscribe
|
10
|
+
# @param mapping (Array<String>) @refer SubscriberConcern.ps_subscribe
|
11
|
+
# @param settings (Hash): @refer SubscriberConcern.ps_subscribe
|
12
|
+
def initialize(klass, action, mapping: [], settings: {})
|
13
|
+
def_settings = { from_klass: klass, to_action: action, id: :id, if: nil, unless: nil, mode: :model }
|
13
14
|
@klass = klass
|
14
|
-
@
|
15
|
-
@attrs = attrs
|
15
|
+
@mapping = mapping
|
16
16
|
@settings = def_settings.merge(settings)
|
17
|
-
@
|
18
|
-
|
19
|
-
|
20
|
-
def process!(payload)
|
21
|
-
@payload = payload
|
22
|
-
if settings[:direct_mode]
|
23
|
-
run_class_message
|
24
|
-
else
|
25
|
-
run_model_message
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def run_class_message
|
32
|
-
model_class = klass.constantize
|
33
|
-
model_class.send(action, payload.data)
|
34
|
-
end
|
35
|
-
|
36
|
-
# support for: create, update, destroy
|
37
|
-
def run_model_message
|
38
|
-
model = find_model
|
39
|
-
return if model.ps_before_save_sync(payload) == :cancel
|
40
|
-
|
41
|
-
if action == :destroy
|
42
|
-
model.destroy!
|
43
|
-
else
|
44
|
-
populate_model(model)
|
45
|
-
return if action == :update && !model.ps_subscriber_changed?(payload.data)
|
46
|
-
|
47
|
-
model.save!
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def find_model
|
52
|
-
model_class = klass.constantize
|
53
|
-
return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
|
54
|
-
|
55
|
-
model_class.where(model_identifiers).first_or_initialize
|
56
|
-
end
|
57
|
-
|
58
|
-
def model_identifiers
|
59
|
-
identifiers.map { |key| [key, payload.data[key.to_sym]] }.to_h
|
60
|
-
end
|
61
|
-
|
62
|
-
def populate_model(model)
|
63
|
-
values = payload.data.slice(*attrs).except(*identifiers)
|
64
|
-
values.each do |attr, value|
|
65
|
-
model.send("#{attr}=", value)
|
66
|
-
end
|
17
|
+
@action = action.to_sym
|
18
|
+
@from_klass = @settings[:from_klass].to_s
|
19
|
+
@mode = @settings[:mode].to_sym
|
67
20
|
end
|
68
21
|
end
|
69
22
|
end
|
@@ -4,46 +4,39 @@ module PubSubModelSync
|
|
4
4
|
module SubscriberConcern
|
5
5
|
def self.included(base)
|
6
6
|
base.extend(ClassMethods)
|
7
|
+
base.send(:attr_accessor, :ps_processing_payload)
|
8
|
+
base.send(:cattr_accessor, :ps_processing_payload)
|
7
9
|
end
|
8
10
|
|
9
|
-
# check if model was changed to skip nonsense .update!()
|
10
|
-
def ps_subscriber_changed?(_data)
|
11
|
-
validate
|
12
|
-
changed?
|
13
|
-
end
|
14
|
-
|
15
|
-
# permit to apply custom actions before applying sync
|
16
|
-
# @return (nil|:cancel): nil to continue sync OR :cancel to skip sync
|
17
|
-
def ps_before_save_sync(_payload); end
|
18
|
-
|
19
11
|
module ClassMethods
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
# @param actions (Symbol|Array<Symbol>) Notification.action name: save|create|update|destroy|<any_other_action>
|
13
|
+
# @param mapping (Array<String>) Attributes mapping with aliasing support, sample: ["id", "full_name:name"]
|
14
|
+
# @param settings (Hash<:from_klass, :to_action, :id, :if, :unless>)
|
15
|
+
# from_klass (String) Notification.class name
|
16
|
+
# to_action (Symbol|Proc):
|
17
|
+
# Symbol: Method to process the notification
|
18
|
+
# Proc: Block to process the notification
|
19
|
+
# id (Symbol|Array<Symbol|String>) attribute(s) DB primary identifier(s). Supports for mapping format.
|
20
|
+
# if (Symbol|Proc|Array<Symbol>) Method or block called as the conformation before calling the callback
|
21
|
+
# unless (Symbol|Proc|Array<Symbol>) Method or block called as the negation before calling the callback
|
22
|
+
def ps_subscribe(actions, mapping = [], settings = {})
|
23
|
+
Array(actions).each do |action|
|
24
|
+
add_ps_subscriber(action, mapping, settings)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
settings
|
32
|
-
add_ps_subscriber(action, nil, settings)
|
33
|
-
end
|
34
|
-
|
35
|
-
def ps_subscriber(action = :create)
|
36
|
-
PubSubModelSync::Config.subscribers.find do |subscriber|
|
37
|
-
subscriber.klass == name && subscriber.action == action
|
38
|
-
end
|
28
|
+
# @param action (Symbol) Notification.action name
|
29
|
+
# @param settings (Hash) @refer ps_subscribe.settings except(:id)
|
30
|
+
def ps_class_subscribe(action, settings = {})
|
31
|
+
add_ps_subscriber(action, nil, settings.merge(mode: :klass))
|
39
32
|
end
|
40
33
|
|
41
34
|
private
|
42
35
|
|
43
36
|
# @param settings (Hash): refer to PubSubModelSync::Subscriber.settings
|
44
|
-
def add_ps_subscriber(action,
|
37
|
+
def add_ps_subscriber(action, mapping, settings = {})
|
45
38
|
klass = PubSubModelSync::Subscriber
|
46
|
-
subscriber = klass.new(name, action,
|
39
|
+
subscriber = klass.new(name, action, mapping: mapping, settings: settings)
|
47
40
|
PubSubModelSync::Config.subscribers.push(subscriber) && subscriber
|
48
41
|
end
|
49
42
|
end
|
@@ -3,6 +3,17 @@
|
|
3
3
|
namespace :pub_sub_model_sync do
|
4
4
|
desc 'Start listening syncs'
|
5
5
|
task start: :environment do
|
6
|
+
# https://github.com/zendesk/ruby-kafka#consumer-groups
|
7
|
+
# Each consumer process will be assigned one or more partitions from each topic that the group
|
8
|
+
# subscribes to. In order to handle more messages, simply start more processes.
|
9
|
+
if PubSubModelSync::Config.service_name == :kafka
|
10
|
+
(PubSubModelSync::ServiceKafka::QTY_WORKERS - 1).times.each do
|
11
|
+
Thread.new do
|
12
|
+
Thread.current.abort_on_exception = true
|
13
|
+
PubSubModelSync::Runner.new.run
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
6
17
|
PubSubModelSync::Runner.new.run
|
7
18
|
end
|
8
19
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class Transaction < Base
|
5
|
+
PUBLISHER_KLASS = PubSubModelSync::MessagePublisher
|
6
|
+
attr_accessor :key, :payloads, :use_buffer, :parent, :children
|
7
|
+
|
8
|
+
# @param key (String|nil) Transaction key, if empty will use the ordering_key from first payload
|
9
|
+
# @param use_buffer (Boolean, default: true) If false, payloads are delivered immediately
|
10
|
+
# (no way to cancel/rollback if transaction failed)
|
11
|
+
def initialize(key, use_buffer: config.transactions_use_buffer)
|
12
|
+
@key = key
|
13
|
+
@use_buffer = use_buffer
|
14
|
+
@children = []
|
15
|
+
@payloads = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param payload (Payload)
|
19
|
+
def add_payload(payload)
|
20
|
+
use_buffer ? payloads << payload : deliver_payload(payload)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deliver_all
|
24
|
+
if parent
|
25
|
+
parent.children = parent.children.reject { |t| t == self }
|
26
|
+
parent.deliver_all
|
27
|
+
end
|
28
|
+
payloads.each(&method(:deliver_payload)) if children.empty?
|
29
|
+
clean_publisher
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_transaction(transaction)
|
33
|
+
transaction.parent = self
|
34
|
+
children << transaction
|
35
|
+
transaction
|
36
|
+
end
|
37
|
+
|
38
|
+
def rollback
|
39
|
+
log("rollback #{children.count} notifications", :warn) if children.any? && debug?
|
40
|
+
self.children = []
|
41
|
+
parent&.rollback
|
42
|
+
clean_publisher
|
43
|
+
end
|
44
|
+
|
45
|
+
def clean_publisher
|
46
|
+
PUBLISHER_KLASS.current_transaction = nil if !parent && children.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def deliver_payload(payload)
|
52
|
+
PUBLISHER_KLASS.connector_publish(payload)
|
53
|
+
rescue => e
|
54
|
+
PUBLISHER_KLASS.send(:notify_error, e, payload)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pub_sub_model_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.beta
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Owen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- ".rubocop.yml"
|
94
94
|
- CHANGELOG.md
|
95
95
|
- CODE_OF_CONDUCT.md
|
96
|
+
- Dockerfile
|
96
97
|
- Gemfile
|
97
98
|
- Gemfile.lock
|
98
99
|
- LICENSE.txt
|
@@ -100,6 +101,8 @@ files:
|
|
100
101
|
- Rakefile
|
101
102
|
- bin/console
|
102
103
|
- bin/setup
|
104
|
+
- docker-compose.yaml
|
105
|
+
- docs/notifications-diagram.png
|
103
106
|
- gemfiles/Gemfile_4
|
104
107
|
- gemfiles/Gemfile_5
|
105
108
|
- gemfiles/Gemfile_6
|
@@ -116,6 +119,7 @@ files:
|
|
116
119
|
- lib/pub_sub_model_sync/publisher.rb
|
117
120
|
- lib/pub_sub_model_sync/publisher_concern.rb
|
118
121
|
- lib/pub_sub_model_sync/railtie.rb
|
122
|
+
- lib/pub_sub_model_sync/run_subscriber.rb
|
119
123
|
- lib/pub_sub_model_sync/runner.rb
|
120
124
|
- lib/pub_sub_model_sync/service_base.rb
|
121
125
|
- lib/pub_sub_model_sync/service_google.rb
|
@@ -124,6 +128,7 @@ files:
|
|
124
128
|
- lib/pub_sub_model_sync/subscriber.rb
|
125
129
|
- lib/pub_sub_model_sync/subscriber_concern.rb
|
126
130
|
- lib/pub_sub_model_sync/tasks/worker.rake
|
131
|
+
- lib/pub_sub_model_sync/transaction.rb
|
127
132
|
- lib/pub_sub_model_sync/version.rb
|
128
133
|
- pub_sub_model_sync.gemspec
|
129
134
|
homepage: https://github.com/owen2345/pub_sub_model_sync
|
@@ -144,9 +149,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
149
|
version: '2.4'
|
145
150
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
151
|
requirements:
|
147
|
-
- - "
|
152
|
+
- - ">"
|
148
153
|
- !ruby/object:Gem::Version
|
149
|
-
version:
|
154
|
+
version: 1.3.1
|
150
155
|
requirements: []
|
151
156
|
rubygems_version: 3.0.8
|
152
157
|
signing_key:
|