startback 0.4.5 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|