startback 0.4.5 → 0.5.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/lib/startback/audit/trailer.rb +145 -0
- data/lib/startback/audit.rb +1 -0
- data/lib/startback/bus/bunny/async.rb +117 -0
- data/lib/startback/bus/bunny.rb +1 -0
- data/lib/startback/bus/memory/async.rb +40 -0
- data/lib/startback/bus/memory/sync.rb +30 -0
- data/lib/startback/bus/memory.rb +2 -0
- data/lib/startback/bus.rb +94 -0
- data/lib/startback/caching/entity_cache.rb +80 -0
- data/lib/startback/caching/store.rb +34 -0
- data/lib/startback/context/middleware.rb +1 -1
- data/lib/startback/context.rb +93 -4
- data/lib/startback/event.rb +43 -0
- data/lib/startback/operation.rb +39 -6
- data/lib/startback/support/fake_logger.rb +18 -0
- data/lib/startback/support/hooks.rb +48 -0
- data/lib/startback/support/operation_runner.rb +150 -0
- data/lib/startback/support/robustness.rb +153 -0
- data/lib/startback/support.rb +3 -0
- data/lib/startback/version.rb +2 -2
- data/lib/startback/web/api.rb +3 -4
- data/lib/startback/web/catch_all.rb +12 -5
- data/lib/startback/web/middleware.rb +13 -0
- data/lib/startback.rb +2 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/audit/test_trailer.rb +88 -0
- data/spec/unit/bus/memory/test_async.rb +41 -0
- data/spec/unit/bus/memory/test_sync.rb +41 -0
- data/spec/unit/caching/test_entity_cache.rb +109 -0
- data/spec/unit/context/test_abstraction_factory.rb +64 -0
- data/spec/unit/support/hooks/test_after_hook.rb +54 -0
- data/spec/unit/support/hooks/test_before_hook.rb +54 -0
- data/spec/unit/support/operation_runner/test_around_run.rb +157 -0
- data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
- data/spec/unit/support/test_robusteness.rb +209 -0
- data/spec/unit/test_context.rb +51 -0
- data/spec/unit/test_event.rb +69 -0
- data/spec/unit/test_operation.rb +0 -3
- metadata +32 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 492399cde5cbfe2861575824a32c7788fc16137902d5a4194b3662e5306a5968
|
4
|
+
data.tar.gz: 192d23a34d668bb3ce495c0bb3c724ef58c4e7857bf8e0ed63d5948a76b43f49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54ff69ead9f9b90eccbc9700a46229621cdfe31eb7b887207f6c69c0f755735ae80fc6257ff9ca6e3430d9fc75a2e04fd7b9ea4ecf6744557cb79d418e1ac3b4
|
7
|
+
data.tar.gz: 71de967d2ebee0cd0596b51532d91a1abc1197cd69d1b6db528fcf6bcd00977c484067a83ba266e37e8d807c3c2bba8efc3b67f822b0540eef9fea3c9578fc5b
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module Startback
|
3
|
+
module Audit
|
4
|
+
#
|
5
|
+
# Log & Audit trail abstraction, that can be registered as an around
|
6
|
+
# hook on OperationRunner and as an actual logger on Context instances.
|
7
|
+
#
|
8
|
+
# The trail is outputted as JSON lines, using a Logger on the "device"
|
9
|
+
# passed at construction. The following JSON entries are dumped:
|
10
|
+
#
|
11
|
+
# - severity : INFO or ERROR
|
12
|
+
# - time : ISO8601 Datetime of operation execution
|
13
|
+
# - op : class name of the operation executed
|
14
|
+
# - op_took : Execution duration of the operation
|
15
|
+
# - op_data : Dump of operation input data
|
16
|
+
# - context : Execution context, through its `h` information contract (IC)
|
17
|
+
#
|
18
|
+
# Dumping of operation data follows the following duck typing conventions:
|
19
|
+
#
|
20
|
+
# - If the operation instance responds to `to_trail`, this data is taken
|
21
|
+
# - If the operation instance responds to `input`, this data is taken
|
22
|
+
# - If the operation instance responds to `request`, this data is taken
|
23
|
+
# - Otherwise op_data is a JSON null
|
24
|
+
#
|
25
|
+
# By contributing to the Context's `h` IC, users can easily dump information that
|
26
|
+
# makes sense (such as the operation execution requester).
|
27
|
+
#
|
28
|
+
# The class implements a sanitization process when dumping the context and
|
29
|
+
# operation data. Blacklisted words taken in construction options are used to
|
30
|
+
# prevent dumping hash keys that match them (insentively). Default stop words
|
31
|
+
# are equivalent to:
|
32
|
+
#
|
33
|
+
# Trailer.new("/var/log/trail.log", {
|
34
|
+
# blacklist: "token password secret credential"
|
35
|
+
# })
|
36
|
+
#
|
37
|
+
# Please note that the sanitization process does not apply recursively if
|
38
|
+
# the operation data is hierarchic. It only applies to the top object of
|
39
|
+
# Hash and [Hash]. Use `Operation#to_trail` to fine-tune your audit trail.
|
40
|
+
#
|
41
|
+
# Given that this Trailer is intended to be used as around hook on an
|
42
|
+
# `OperationRunner`, operations that fail at construction time will not be
|
43
|
+
# trailed at all, since they can't be ran in the first place. This may lead
|
44
|
+
# to trails not containing important errors cases if operations check their
|
45
|
+
# input at construction time.
|
46
|
+
#
|
47
|
+
class Trailer
|
48
|
+
extend Forwardable
|
49
|
+
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
50
|
+
|
51
|
+
class Formatter
|
52
|
+
|
53
|
+
def call(severity, time, progname, msg)
|
54
|
+
if msg[:error] && msg[:error].respond_to?(:message, true)
|
55
|
+
msg[:error] = msg[:error].message
|
56
|
+
end
|
57
|
+
{
|
58
|
+
severity: severity,
|
59
|
+
time: time,
|
60
|
+
}.merge(msg).to_json << "\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
end # class Formatter
|
64
|
+
|
65
|
+
DEFAULT_OPTIONS = {
|
66
|
+
|
67
|
+
# Words used to stop dumping for, e.g., security reasons
|
68
|
+
blacklist: "token password secret credential"
|
69
|
+
|
70
|
+
}
|
71
|
+
|
72
|
+
def initialize(device, options = {})
|
73
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
74
|
+
@logger = ::Logger.new(device, 'daily')
|
75
|
+
@logger.formatter = Formatter.new
|
76
|
+
end
|
77
|
+
attr_reader :logger, :options
|
78
|
+
|
79
|
+
def call(runner, op)
|
80
|
+
result = nil
|
81
|
+
time = Benchmark.realtime{ result = yield }
|
82
|
+
logger.info(op_to_trail(op, time))
|
83
|
+
result
|
84
|
+
rescue => ex
|
85
|
+
logger.error(op_to_trail(op, time, ex))
|
86
|
+
raise
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def op_to_trail(op, time, ex = nil)
|
92
|
+
log_msg = {
|
93
|
+
op_took: time ? time.round(8) : nil,
|
94
|
+
op: op_name(op),
|
95
|
+
context: op_context(op),
|
96
|
+
op_data: op_data(op)
|
97
|
+
}
|
98
|
+
log_msg[:error] = ex if ex
|
99
|
+
log_msg
|
100
|
+
end
|
101
|
+
|
102
|
+
def op_name(op)
|
103
|
+
case op
|
104
|
+
when String then op
|
105
|
+
when Class then op.name
|
106
|
+
else op.class.name
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def op_context(op)
|
111
|
+
sanitize(op.respond_to?(:context, false) ? op.context.to_h : {})
|
112
|
+
end
|
113
|
+
|
114
|
+
def op_data(op)
|
115
|
+
data = if op.respond_to?(:to_trail, false)
|
116
|
+
op.to_trail
|
117
|
+
elsif op.respond_to?(:input, false)
|
118
|
+
op.input
|
119
|
+
elsif op.respond_to?(:request, false)
|
120
|
+
op.request
|
121
|
+
end
|
122
|
+
sanitize(data)
|
123
|
+
end
|
124
|
+
|
125
|
+
def sanitize(data)
|
126
|
+
case data
|
127
|
+
when Hash, OpenStruct
|
128
|
+
data.dup.delete_if{|k| k.to_s =~ blacklist_rx }
|
129
|
+
when Enumerable
|
130
|
+
data.map{|elm| sanitize(elm) }.compact
|
131
|
+
else
|
132
|
+
data
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def blacklist_rx
|
137
|
+
@blacklist_rx ||= Regexp.new(
|
138
|
+
options[:blacklist].split(/\s+/).join("|"),
|
139
|
+
Regexp::IGNORECASE
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
end # class Trailer
|
144
|
+
end # module Audit
|
145
|
+
end # module Startback
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'audit/trailer'
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
module Startback
|
3
|
+
class Bus
|
4
|
+
module Bunny
|
5
|
+
#
|
6
|
+
# Asynchronous implementation of the bus abstraction, on top of RabbitMQ
|
7
|
+
# and using the 'bunny' gem (you need to include it in your Gemfile
|
8
|
+
# yourself: it is NOT a startback official dependency).
|
9
|
+
#
|
10
|
+
# This bus implementation emits events by dumping them to RabbitMQ using
|
11
|
+
# the event type as exchange name. Listeners may use the `processor`
|
12
|
+
# parameter to specify the queue name ; otherwise a default "main" queue
|
13
|
+
# is used.
|
14
|
+
#
|
15
|
+
# Examples:
|
16
|
+
#
|
17
|
+
# # Connects to RabbitMQ using all default options
|
18
|
+
# #
|
19
|
+
# # Uses the STARTBACK_BUS_BUNNY_ASYNC_URL environment variable for
|
20
|
+
# # connection URL if present.
|
21
|
+
# Startback::Bus::Bunny::Async.new
|
22
|
+
#
|
23
|
+
# # Connects to RabbitMQ using a specific URL
|
24
|
+
# Startback::Bus::Bunny::Async.new("amqp://rabbituser:rabbitpass@192.168.17.17")
|
25
|
+
# Startback::Bus::Bunny::Async.new(url: "amqp://rabbituser:rabbitpass@192.168.17.17")
|
26
|
+
#
|
27
|
+
# # Connects to RabbitMQ using specific connection options. See Bunny's own
|
28
|
+
# # documentation
|
29
|
+
# Startback::Bus::Bunny::Async.new({
|
30
|
+
# connection_options: {
|
31
|
+
# host: "192.168.17.17"
|
32
|
+
# }
|
33
|
+
# })
|
34
|
+
#
|
35
|
+
class Async
|
36
|
+
include Support::Robustness
|
37
|
+
|
38
|
+
DEFAULT_OPTIONS = {
|
39
|
+
# (optional) The URL to use for connecting to RabbitMQ.
|
40
|
+
url: ENV['STARTBACK_BUS_BUNNY_ASYNC_URL'],
|
41
|
+
|
42
|
+
# (optional) The options has to pass to ::Bunny constructor
|
43
|
+
connection_options: nil,
|
44
|
+
|
45
|
+
# (optional) The options to use for the emitter/listener fanout
|
46
|
+
fanout_options: {},
|
47
|
+
|
48
|
+
# (optional) The options to use for the listener queue
|
49
|
+
queue_options: {},
|
50
|
+
|
51
|
+
# (optional) Default event factory to use, if any
|
52
|
+
event_factory: nil,
|
53
|
+
|
54
|
+
# (optional) A default context to use for general logging
|
55
|
+
context: nil
|
56
|
+
}
|
57
|
+
|
58
|
+
# Creates a bus instance, using the various options provided to
|
59
|
+
# fine-tune behavior.
|
60
|
+
def initialize(options = {})
|
61
|
+
options = { url: options } if options.is_a?(String)
|
62
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
63
|
+
retried = 0
|
64
|
+
conn = options[:connection_options] || options[:url]
|
65
|
+
try_max_times(10) do
|
66
|
+
@bunny = ::Bunny.new(conn)
|
67
|
+
@bunny.start
|
68
|
+
@channel = @bunny.create_channel
|
69
|
+
log(:info, {op: "#{self.class.name}#connect", op_data: conn}, options[:context])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
attr_reader :channel, :options
|
73
|
+
|
74
|
+
def emit(event)
|
75
|
+
stop_errors(self, "emit", event.context) do
|
76
|
+
fanout = channel.fanout(event.type.to_s, fanout_options)
|
77
|
+
fanout.publish(event.to_json)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def listen(type, processor = nil, listener = nil, &bl)
|
82
|
+
raise ArgumentError, "A listener must be provided" unless listener || bl
|
83
|
+
fanout = channel.fanout(type.to_s, fanout_options)
|
84
|
+
queue = channel.queue((processor || "main").to_s, queue_options)
|
85
|
+
queue.bind(fanout)
|
86
|
+
queue.subscribe do |delivery_info, properties, body|
|
87
|
+
event = stop_errors(self, "listen") do
|
88
|
+
factor_event(body)
|
89
|
+
end
|
90
|
+
stop_errors(self, "listen", event.context) do
|
91
|
+
(listener || bl).call(event)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def fanout_options
|
99
|
+
options[:fanout_options]
|
100
|
+
end
|
101
|
+
|
102
|
+
def queue_options
|
103
|
+
options[:queue_options]
|
104
|
+
end
|
105
|
+
|
106
|
+
def factor_event(body)
|
107
|
+
if options[:event_factory]
|
108
|
+
options[:event_factory].call(body)
|
109
|
+
else
|
110
|
+
Event.json(body, options)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end # class Async
|
115
|
+
end # module Bunny
|
116
|
+
end # class Bus
|
117
|
+
end # module Klaro
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'bunny/async'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Startback
|
2
|
+
class Bus
|
3
|
+
module Memory
|
4
|
+
#
|
5
|
+
# Asynchronous implementation of the Bus abstraction, for use between
|
6
|
+
# components sharing the same process.
|
7
|
+
#
|
8
|
+
# This implementation actually calls listeners synchronously (it mays)
|
9
|
+
# but hides error raised by them. See Bus::Bunny::Async for another
|
10
|
+
# implementation that is truly asynchronous and relies on RabbitMQ.
|
11
|
+
#
|
12
|
+
class Async
|
13
|
+
include Support::Robustness
|
14
|
+
|
15
|
+
DEFAULT_OPTIONS = {
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
20
|
+
@listeners = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def emit(event)
|
24
|
+
(@listeners[event.type.to_s] || []).each do |l|
|
25
|
+
stop_errors(self, "emit", event) {
|
26
|
+
l.call(event)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def listen(type, processor = nil, listener = nil, &bl)
|
32
|
+
raise ArgumentError, "A listener must be provided" unless listener || bl
|
33
|
+
@listeners[type.to_s] ||= []
|
34
|
+
@listeners[type.to_s] << (listener || bl)
|
35
|
+
end
|
36
|
+
|
37
|
+
end # class Sync
|
38
|
+
end # module Memory
|
39
|
+
end # class Bus
|
40
|
+
end # module Klaro
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Startback
|
2
|
+
class Bus
|
3
|
+
module Memory
|
4
|
+
#
|
5
|
+
# Synchronous implementation of the Bus abstraction, for use between
|
6
|
+
# components sharing the same process.
|
7
|
+
#
|
8
|
+
class Sync
|
9
|
+
include Support::Robustness
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@listeners = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def emit(event)
|
16
|
+
(@listeners[event.type.to_s] || []).each do |l|
|
17
|
+
l.call(event)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def listen(type, processor = nil, listener = nil, &bl)
|
22
|
+
raise ArgumentError, "A listener must be provided" unless listener || bl
|
23
|
+
@listeners[type.to_s] ||= []
|
24
|
+
@listeners[type.to_s] << (listener || bl)
|
25
|
+
end
|
26
|
+
|
27
|
+
end # class Sync
|
28
|
+
end # module Memory
|
29
|
+
end # class Bus
|
30
|
+
end # module Klaro
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'startback/event'
|
2
|
+
module Startback
|
3
|
+
#
|
4
|
+
# Sync and async bus abstraction allowing to register listeners and
|
5
|
+
# emitting events towards them.
|
6
|
+
#
|
7
|
+
# This bus actually decorates two busses, one in synchronous and the
|
8
|
+
# other one is asynchronous (optional).
|
9
|
+
#
|
10
|
+
# * A synchronous bus MUST call the listeners as part of emitting
|
11
|
+
# process, and MUST re-raise any error occuring during that process.
|
12
|
+
# See, e.g. Startback::Bus::Memory::Sync
|
13
|
+
#
|
14
|
+
# * An asynchronous bus MAY call the listeners later, but MUST hide
|
15
|
+
# errors to the emitter.
|
16
|
+
# See, e.g. Startback::Bus::Memory::Async
|
17
|
+
#
|
18
|
+
# This bus facade emits events to both sync and async busses (if any),
|
19
|
+
# and listen on the sync one by default.
|
20
|
+
#
|
21
|
+
# For emitters:
|
22
|
+
#
|
23
|
+
# # This will synchronously call every listeners who `listen`
|
24
|
+
# # on the synchronous bus (& reraise exceptions) then call
|
25
|
+
# # (possibly later) all listeners who `listen` on the
|
26
|
+
# # asynchronous bus if any (& hide exceptions).
|
27
|
+
# bus.emit(event)
|
28
|
+
#
|
29
|
+
# # This only reaches sync listeners
|
30
|
+
# bus.sync.emit(event)
|
31
|
+
#
|
32
|
+
# # This only reaches async listeners (an async bus must be set)
|
33
|
+
# bus.async.emit(event)
|
34
|
+
#
|
35
|
+
# Please note that there is currently no way to reach sync listeners
|
36
|
+
# without having to implement error handling on the emitter side.
|
37
|
+
#
|
38
|
+
# For listeners:
|
39
|
+
#
|
40
|
+
# # This will listen synchronously and make the emitter fail if
|
41
|
+
# # anything goes wrong with the callback:
|
42
|
+
# bus.listen(event_type) do |event|
|
43
|
+
# ...
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # It is a shortcut for:
|
47
|
+
# bus.sync.listen(event_type) do |event| ... end
|
48
|
+
#
|
49
|
+
# This will listen asynchronously and could not make the emitter
|
50
|
+
# fail if something goes wrong with the callback.
|
51
|
+
# bus.async.listen(event_type) do |event|
|
52
|
+
# ...
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Feel free to access the sync and async busses directly for specific
|
56
|
+
# cases though.
|
57
|
+
#
|
58
|
+
class Bus
|
59
|
+
include Support::Robustness
|
60
|
+
|
61
|
+
def initialize(sync = Memory::Sync.new, async = nil)
|
62
|
+
@sync = sync
|
63
|
+
@async = async
|
64
|
+
end
|
65
|
+
attr_reader :sync, :async
|
66
|
+
|
67
|
+
# Emits a particular event to the listeners.
|
68
|
+
#
|
69
|
+
# @arg event an event, should be an Event instance (through duck
|
70
|
+
# typing is allowed)
|
71
|
+
def emit(event)
|
72
|
+
monitor({
|
73
|
+
op: "Startback::Bus#emit",
|
74
|
+
op_data: {
|
75
|
+
event: { type: event.type }
|
76
|
+
}
|
77
|
+
}, event.context) do
|
78
|
+
sync.emit(event)
|
79
|
+
async.emit(event) if async
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Registers `listener` as being interested in receiving events of
|
84
|
+
# a specific type.
|
85
|
+
#
|
86
|
+
# @arg type: Symbol, the type of event the listener is interested in.
|
87
|
+
# @arg listener: Proc, the listener itself.
|
88
|
+
def listen(type, processor = nil, listener = nil, &bl)
|
89
|
+
sync.listen(type, processor, listener, &bl)
|
90
|
+
end
|
91
|
+
|
92
|
+
end # class Bus
|
93
|
+
end # module Klaro
|
94
|
+
require_relative 'bus/memory'
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Startback
|
2
|
+
module Caching
|
3
|
+
#
|
4
|
+
# A overriable caching abstraction aiming at making Entity-based caching easy.
|
5
|
+
#
|
6
|
+
# This class MUST be overriden:
|
7
|
+
#
|
8
|
+
# * the `load_raw_data` protected method MUST be implemented.
|
9
|
+
# * the `full_key` protected method MAY be overriden to provide specific caching
|
10
|
+
# keys, e.g. by using the context.
|
11
|
+
# * the `valid?` protected method MAY be overriden to check validity of data
|
12
|
+
# extracted from the cache.
|
13
|
+
#
|
14
|
+
# An EntityCache takes an actual store at construction. The object must meet the
|
15
|
+
# specification writtern in Store. The 'cache' ruby gem can be used in practice.
|
16
|
+
#
|
17
|
+
class EntityCache
|
18
|
+
|
19
|
+
class << self
|
20
|
+
|
21
|
+
# Default time to live, in seconds
|
22
|
+
attr_writer :default_ttl
|
23
|
+
|
24
|
+
def default_ttl
|
25
|
+
@default_ttl || (superclass.respond_to?(:default_ttl, true) && superclass.default_ttl) || 3600
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class DSL
|
29
|
+
|
30
|
+
def initialize(store, context = nil)
|
31
|
+
@store = store
|
32
|
+
@context = context
|
33
|
+
end
|
34
|
+
attr_reader :store, :context
|
35
|
+
|
36
|
+
# Returns the entity corresponding to a given key.
|
37
|
+
#
|
38
|
+
# If the entity is not in cache, loads it and puts it in cache using
|
39
|
+
# the caching options passed as second parameter.
|
40
|
+
def get(short_key, caching_options = default_caching_options)
|
41
|
+
cache_key = encode_key(full_key(short_key))
|
42
|
+
if store.exist?(cache_key)
|
43
|
+
cached = store.get(cache_key)
|
44
|
+
return cached if valid?(cache_key, cached)
|
45
|
+
end
|
46
|
+
load_raw_data(short_key).tap{|to_cache|
|
47
|
+
store.set(cache_key, to_cache, caching_options)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Invalidates the cache under a given key.
|
52
|
+
def invalidate(key)
|
53
|
+
store.delete(encode_key(full_key(key)))
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def encode_key(key)
|
59
|
+
JSON.fast_generate(key)
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_caching_options
|
63
|
+
{ ttl: self.class.default_ttl }
|
64
|
+
end
|
65
|
+
|
66
|
+
def valid?(cache_key, cached)
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def full_key(key)
|
71
|
+
key
|
72
|
+
end
|
73
|
+
|
74
|
+
def load_raw_data(short_key)
|
75
|
+
raise NotImplementedError, "#{self.class.name}#load_raw_data"
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class EntityCache
|
79
|
+
end # module Caching
|
80
|
+
end # module Startback
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Startback
|
2
|
+
module Caching
|
3
|
+
#
|
4
|
+
# Caching store specification & dummy implementation.
|
5
|
+
#
|
6
|
+
# This class should not be used in real project, as it implements
|
7
|
+
# See the 'cache' gem that provides conforming implementations.
|
8
|
+
#
|
9
|
+
class Store
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@saved = {}
|
13
|
+
end
|
14
|
+
attr_reader :saved
|
15
|
+
|
16
|
+
def exist?(key)
|
17
|
+
saved.has_key?(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(key)
|
21
|
+
saved[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def set(key, value, ttl)
|
25
|
+
saved[key] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
saved.delete(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
end # class Store
|
33
|
+
end # module Caching
|
34
|
+
end # module Startback
|
data/lib/startback/context.rb
CHANGED
@@ -1,13 +1,35 @@
|
|
1
1
|
module Startback
|
2
2
|
#
|
3
|
-
# Defines an execution context for Startback applications
|
4
|
-
#
|
5
|
-
# This class is aimed at being subclassed for application required
|
6
|
-
# extension.
|
3
|
+
# Defines an execution context for Startback applications, and provides
|
4
|
+
# a cached factory for related abstractions (see `factor`).
|
7
5
|
#
|
8
6
|
# In web application, an instance of a context can be set on the Rack
|
9
7
|
# environment, using Context::Middleware.
|
10
8
|
#
|
9
|
+
# This class SHOULD be subclassed for application required extensions
|
10
|
+
# to prevent touching the global Startback state itself.
|
11
|
+
#
|
12
|
+
# Also, for event handling in distributed architectures, a Context should
|
13
|
+
# be dumpable and reloadable to JSON. An `h` information contract if provided
|
14
|
+
# for that. Subclasses may contribute to the dumping and reloading process
|
15
|
+
# through the `h_dump` and `h_factory` methods
|
16
|
+
#
|
17
|
+
# module MyApp
|
18
|
+
# class Context < Startback::Context
|
19
|
+
#
|
20
|
+
# attr_accessor :foo
|
21
|
+
#
|
22
|
+
# h_dump do |h|
|
23
|
+
# h.merge!("foo" => foo)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# h_factor do |c,h|
|
27
|
+
# c.foo = h["foo"]
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
11
33
|
class Context
|
12
34
|
attr_accessor :original_rack_env
|
13
35
|
|
@@ -16,8 +38,75 @@ module Startback
|
|
16
38
|
# instance, simply.
|
17
39
|
#
|
18
40
|
# Fatal errors catched by Web::CatchAll are sent on `error_handler#fatal`
|
41
|
+
#
|
42
|
+
# Deprecated, use the logger below instead.
|
19
43
|
attr_accessor :error_handler
|
20
44
|
|
45
|
+
# A logger can be provided on the context, and will be used for everything
|
46
|
+
# related to logging, audit trailing and robustness. The logger receives
|
47
|
+
# object following the log & trail conventions of Startback, and must
|
48
|
+
# convert them to wathever log format is necessary.
|
49
|
+
attr_accessor :logger
|
50
|
+
|
51
|
+
# Implementation of the `h` information contract
|
52
|
+
class << self
|
53
|
+
|
54
|
+
def h(hash)
|
55
|
+
h_factor!(self.new, hash)
|
56
|
+
end
|
57
|
+
|
58
|
+
def h_factor!(context, hash)
|
59
|
+
h_factories.each do |f|
|
60
|
+
f.call(context, hash)
|
61
|
+
end
|
62
|
+
context
|
63
|
+
end
|
64
|
+
|
65
|
+
def h_factories
|
66
|
+
@h_factories ||= []
|
67
|
+
end
|
68
|
+
|
69
|
+
def h_factory(&factory)
|
70
|
+
h_factories << factory
|
71
|
+
end
|
72
|
+
|
73
|
+
###
|
74
|
+
|
75
|
+
def h_dump!(context, hash = {})
|
76
|
+
h_dumpers.each do |d|
|
77
|
+
context.instance_exec(hash, &d)
|
78
|
+
end
|
79
|
+
hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def h_dumpers
|
83
|
+
@h_dumpers ||= []
|
84
|
+
end
|
85
|
+
|
86
|
+
def h_dump(&dumper)
|
87
|
+
h_dumpers << dumper
|
88
|
+
end
|
89
|
+
|
90
|
+
end # class << self
|
91
|
+
|
92
|
+
# Factors an instance of `clazz`, which must be a Context-related
|
93
|
+
# abstraction (i.e. its constructor takes the context as last parameters).
|
94
|
+
#
|
95
|
+
# Factored abstractions are cached for a given context & arguments.
|
96
|
+
def factor(clazz, *args)
|
97
|
+
@factored ||= {}
|
98
|
+
key = args.empty? ? clazz : [clazz] + args
|
99
|
+
@factored[key] ||= clazz.new(*(args << self))
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_h
|
103
|
+
self.class.h_dump!(self)
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_json(*args, &bl)
|
107
|
+
to_h.to_json(*args, &bl)
|
108
|
+
end
|
109
|
+
|
21
110
|
end # class Context
|
22
111
|
end # module Startback
|
23
112
|
require_relative 'context/middleware'
|