alephant-broker 3.14.0 → 3.15.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/.travis.yml +22 -9
- data/README.md +35 -6
- data/alephant-broker.gemspec +3 -2
- data/lib/alephant/broker/cache.rb +1 -0
- data/lib/alephant/broker/cache/cached_object.rb +71 -0
- data/lib/alephant/broker/cache/client.rb +29 -17
- data/lib/alephant/broker/cache/null_client.rb +12 -4
- data/lib/alephant/broker/component.rb +3 -1
- data/lib/alephant/broker/component_meta.rb +2 -0
- data/lib/alephant/broker/error_component.rb +1 -1
- data/lib/alephant/broker/load_strategy.rb +1 -0
- data/lib/alephant/broker/load_strategy/http.rb +2 -3
- data/lib/alephant/broker/load_strategy/revalidate/fetcher.rb +51 -0
- data/lib/alephant/broker/load_strategy/revalidate/refresher.rb +75 -0
- data/lib/alephant/broker/load_strategy/revalidate/strategy.rb +86 -0
- data/lib/alephant/broker/load_strategy/s3/base.rb +2 -2
- data/lib/alephant/broker/response/asset.rb +1 -1
- data/lib/alephant/broker/response/base.rb +7 -8
- data/lib/alephant/broker/response/batch.rb +1 -1
- data/lib/alephant/broker/version.rb +1 -1
- data/spec/alephant/broker/cache/cached_object_spec.rb +107 -0
- data/spec/alephant/broker/load_strategy/http_spec.rb +5 -7
- data/spec/alephant/broker/load_strategy/revalidate/fetcher_spec.rb +88 -0
- data/spec/alephant/broker/load_strategy/revalidate/refresher_spec.rb +69 -0
- data/spec/alephant/broker/load_strategy/revalidate/strategy_spec.rb +185 -0
- data/spec/fixtures/json/batch_compiled_no_sequence.json +1 -0
- data/spec/integration/not_modified_response_spec.rb +4 -6
- data/spec/integration/rack_spec.rb +0 -3
- data/spec/integration/revalidate_spec.rb +174 -0
- data/spec/spec_helper.rb +1 -0
- metadata +99 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18fa7932242b113184d3dd00fcea6fd0700d6bd8
|
4
|
+
data.tar.gz: 4b8c2034dfe9275d18aa1163bc65bd47d8463cb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3183c49f2222401e560b929fed1887e3b67c169bff1b33764489955a7cf14fcb144d40d79e9006cb1f42d282989ee1cafec562bfebb81845a42577f3c76df24e
|
7
|
+
data.tar.gz: 5fe75198d006255e0573c4ac4fec30e49b622e75f4059ea1089edcc53fa7f61cfebd12b7c79ab9363b59ba25f61f9b31b7900e6be28457351399a518c4831b89
|
data/.travis.yml
CHANGED
@@ -1,11 +1,24 @@
|
|
1
|
+
sudo: false
|
2
|
+
cache: bundler
|
1
3
|
language: ruby
|
4
|
+
|
5
|
+
# Ruby build matrix
|
2
6
|
rvm:
|
3
|
-
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
- 2.0
|
8
|
+
- 2.1
|
9
|
+
- 2.2
|
10
|
+
- 2.3.0
|
11
|
+
- 2.3.1
|
12
|
+
- jruby
|
13
|
+
|
14
|
+
# Environment variables
|
15
|
+
env:
|
16
|
+
- RUBYOPT="-W0"
|
17
|
+
|
18
|
+
# Ensure we don't build for *every* commit (doesn't apply to PR builds)
|
19
|
+
branches:
|
20
|
+
only:
|
21
|
+
- master
|
22
|
+
|
23
|
+
script:
|
24
|
+
- bundle exec rspec --format documentation
|
data/README.md
CHANGED
@@ -28,7 +28,6 @@ The **Broker** is capable of retrieving rendered templates from either [S3](http
|
|
28
28
|
|
29
29
|
```ruby
|
30
30
|
require 'alephant/broker'
|
31
|
-
require 'alephant/broker/load_strategy/s3'
|
32
31
|
|
33
32
|
config = {
|
34
33
|
:s3_bucket_id => 'test_bucket',
|
@@ -55,7 +54,6 @@ end
|
|
55
54
|
|
56
55
|
```ruby
|
57
56
|
require 'alephant/broker'
|
58
|
-
require 'alephant/broker/load_strategy/http'
|
59
57
|
|
60
58
|
class UrlGenerator < Alephant::Broker::LoadStrategy::HTTP::URL
|
61
59
|
def generate(id, options)
|
@@ -87,7 +85,7 @@ The HTML load strategy relies upon being given a URLGenerator, which is used to
|
|
87
85
|
* include a `#generate` method which takes `id` (string) and `options` (hash) as parameters.
|
88
86
|
|
89
87
|
```ruby
|
90
|
-
require 'alephant/broker
|
88
|
+
require 'alephant/broker'
|
91
89
|
require 'rack'
|
92
90
|
|
93
91
|
class UrlGenerator < Alephant::Broker::LoadStrategy::HTTP::URL
|
@@ -103,6 +101,40 @@ class UrlGenerator < Alephant::Broker::LoadStrategy::HTTP::URL
|
|
103
101
|
end
|
104
102
|
```
|
105
103
|
|
104
|
+
##### Revalidate Strategy
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
require 'alephant/broker'
|
108
|
+
|
109
|
+
config = {
|
110
|
+
:s3_bucket_id => 'test_bucket',
|
111
|
+
:s3_object_path => 'foo',
|
112
|
+
:lookup_table_name => 'test_lookup',
|
113
|
+
:sqs_queue_name => 'test_queue',
|
114
|
+
:elasticache_config_endpoint => 'test_elisticache'
|
115
|
+
:revalidate_cache_ttl => 30
|
116
|
+
}
|
117
|
+
|
118
|
+
Alephant::Broker::Application.new(
|
119
|
+
Alephant::Broker::LoadStrategy::Revalidate::Strategy.new,
|
120
|
+
config
|
121
|
+
).call(request).tap do |response|
|
122
|
+
puts "status: #{response.status}"
|
123
|
+
puts "content: #{response.content}"
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
The "Revalidate" strategy uses the following process flow:
|
128
|
+
|
129
|
+

|
130
|
+
|
131
|
+
And uses the following AWS resources:
|
132
|
+
|
133
|
+
* S3 - for storage of rendered components
|
134
|
+
* DynamoDB - for recording S3 component locations
|
135
|
+
* Elasticache - for caching S3 content and recording "inflight" messages
|
136
|
+
* SQS - for communication between the broker and renderer
|
137
|
+
|
106
138
|
### Rack App
|
107
139
|
|
108
140
|
Create **config.ru** using example below, and then run:
|
@@ -144,6 +176,3 @@ This version is added as a header to the response in the following format:
|
|
144
176
|
5. Create a new [Pull Request](https://github.com/BBC-News/alephant-broker/pulls).
|
145
177
|
|
146
178
|
Feel free to create a new [issue](https://github.com/BBC-News/alephant-broker/issues/new) if you find a bug.
|
147
|
-
|
148
|
-
|
149
|
-
|
data/alephant-broker.gemspec
CHANGED
@@ -26,13 +26,14 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "pry-remote"
|
27
27
|
spec.add_development_dependency "pry-nav"
|
28
28
|
spec.add_development_dependency "rake-rspec", ">= 0.0.2"
|
29
|
-
|
30
29
|
spec.add_development_dependency "bundler", "~> 1.5"
|
31
30
|
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency 'rack', '< 2.0.0' # NOTE: rack 2.0 requires Ruby 2.2+
|
32
32
|
spec.add_development_dependency "rack-test"
|
33
33
|
spec.add_development_dependency "simplecov"
|
34
|
+
spec.add_development_dependency "timecop"
|
34
35
|
|
35
|
-
spec.add_runtime_dependency "alephant-lookup"
|
36
|
+
spec.add_runtime_dependency "alephant-lookup", ">= 2.0.2"
|
36
37
|
spec.add_runtime_dependency "alephant-storage", ">= 1.1.1"
|
37
38
|
spec.add_runtime_dependency "alephant-logger", "1.2.0"
|
38
39
|
spec.add_runtime_dependency 'alephant-sequencer'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'alephant/logger'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Alephant
|
5
|
+
module Broker
|
6
|
+
module Cache
|
7
|
+
class CachedObject
|
8
|
+
include Logger
|
9
|
+
attr_reader :s3_obj
|
10
|
+
|
11
|
+
DEFAULT_TTL = 10
|
12
|
+
|
13
|
+
def initialize(obj)
|
14
|
+
logger.info(event: 'SettingCachedObject',
|
15
|
+
content: obj,
|
16
|
+
method: "#{self.class}#initialize")
|
17
|
+
|
18
|
+
@s3_obj = obj
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(obj)
|
22
|
+
logger.info(event: 'UpdatingCachedObject',
|
23
|
+
old_content: @s3_obj,
|
24
|
+
new_content: obj,
|
25
|
+
method: "#{self.class}#update")
|
26
|
+
|
27
|
+
@s3_obj = obj
|
28
|
+
end
|
29
|
+
|
30
|
+
def updated
|
31
|
+
time = metadata[:'head_Last-Modified']
|
32
|
+
Time.parse(time)
|
33
|
+
rescue TypeError, ArgumentError => error
|
34
|
+
logger.info(event: 'ErrorCaught', method: "#{self.class}#updated", error: error)
|
35
|
+
Time.now
|
36
|
+
end
|
37
|
+
|
38
|
+
def ttl
|
39
|
+
Integer(metadata[:ttl] || metadata['ttl'])
|
40
|
+
rescue TypeError => error
|
41
|
+
logger.info(event: 'ErrorCaught', method: "#{self.class}#ttl", error: error)
|
42
|
+
Integer(Broker.config[:revalidate_cache_ttl] || DEFAULT_TTL)
|
43
|
+
end
|
44
|
+
|
45
|
+
def expired?
|
46
|
+
result = (updated + ttl) < Time.now
|
47
|
+
|
48
|
+
logger.info(event: 'Expired?',
|
49
|
+
updated: updated,
|
50
|
+
ttl: ttl,
|
51
|
+
updated_plus_ttl: (updated + ttl),
|
52
|
+
now: Time.now,
|
53
|
+
result: result,
|
54
|
+
method: "#{self.class}#expired?")
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h(obj = nil)
|
60
|
+
obj || s3_obj
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def metadata
|
66
|
+
s3_obj.fetch(:meta, {})
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'dalli-elasticache'
|
2
|
+
require 'alephant/logger'
|
3
3
|
|
4
4
|
module Alephant
|
5
5
|
module Broker
|
@@ -11,45 +11,57 @@ module Alephant
|
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
if config_endpoint.nil?
|
14
|
-
logger.debug
|
15
|
-
logger.metric
|
14
|
+
logger.debug 'Broker::Cache::Client#initialize: No config endpoint, NullClient used'
|
15
|
+
logger.metric 'NoConfigEndpoint'
|
16
16
|
@client = NullClient.new
|
17
17
|
else
|
18
|
-
@elasticache ||= ::Dalli::ElastiCache.new(config_endpoint, :
|
18
|
+
@elasticache ||= ::Dalli::ElastiCache.new(config_endpoint, expires_in: ttl)
|
19
19
|
@client ||= @elasticache.client
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def get(key)
|
24
|
-
versioned_key = versioned
|
25
|
-
result
|
26
|
-
|
27
|
-
logger.
|
28
|
-
|
29
|
-
|
24
|
+
versioned_key = versioned(key)
|
25
|
+
result = @client.get(versioned_key)
|
26
|
+
|
27
|
+
logger.info("Broker::Cache::Client#get key: #{versioned_key} - #{result ? 'hit' : 'miss'}")
|
28
|
+
logger.metric('GetKeyMiss') unless result
|
29
|
+
|
30
|
+
return result if result
|
31
|
+
|
32
|
+
set(key, yield) if block_given?
|
33
|
+
rescue StandardError => error
|
34
|
+
logger.info(event: 'ErrorCaught', method: "#{self.class}#get", error: error)
|
30
35
|
yield if block_given?
|
31
36
|
end
|
32
37
|
|
33
|
-
def set(key, value,
|
34
|
-
|
38
|
+
def set(key, value, custom_ttl = nil)
|
39
|
+
versioned_key = versioned(key)
|
40
|
+
set_ttl = custom_ttl || ttl
|
41
|
+
|
42
|
+
logger.info("#{self.class}#set - key: #{versioned_key}, value: #{value}, ttl: #{set_ttl}")
|
43
|
+
|
44
|
+
@client.set(versioned_key, value, set_ttl)
|
45
|
+
|
46
|
+
value
|
35
47
|
end
|
36
48
|
|
37
49
|
private
|
38
50
|
|
39
51
|
def config_endpoint
|
40
|
-
Broker.config[
|
52
|
+
Broker.config[:elasticache_config_endpoint] || Broker.config['elasticache_config_endpoint']
|
41
53
|
end
|
42
54
|
|
43
55
|
def ttl
|
44
|
-
Broker.config[
|
56
|
+
Broker.config[:elasticache_ttl] || Broker.config['elasticache_ttl'] || DEFAULT_TTL
|
45
57
|
end
|
46
58
|
|
47
59
|
def versioned(key)
|
48
|
-
[key, cache_version].compact.join(
|
60
|
+
[key, cache_version].compact.join('_')
|
49
61
|
end
|
50
62
|
|
51
63
|
def cache_version
|
52
|
-
Broker.config[
|
64
|
+
Broker.config[:elasticache_cache_version] || Broker.config['elasticache_cache_version']
|
53
65
|
end
|
54
66
|
end
|
55
67
|
end
|
@@ -2,12 +2,20 @@ module Alephant
|
|
2
2
|
module Broker
|
3
3
|
module Cache
|
4
4
|
class NullClient
|
5
|
-
def
|
6
|
-
|
5
|
+
def initialize
|
6
|
+
@cache = {}
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def get(key)
|
10
|
+
data = @cache[key]
|
11
|
+
|
12
|
+
return data if data
|
13
|
+
|
14
|
+
set(key, yield) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def set(key, value, _ttl = nil)
|
18
|
+
@cache[key] = value
|
11
19
|
end
|
12
20
|
end
|
13
21
|
end
|
@@ -23,7 +23,7 @@ module Alephant
|
|
23
23
|
fetch_object(component_meta)
|
24
24
|
rescue
|
25
25
|
logger.metric "CacheMiss"
|
26
|
-
cache.set(component_meta.
|
26
|
+
cache.set(component_meta.component_key, content(component_meta))
|
27
27
|
end
|
28
28
|
|
29
29
|
private
|
@@ -35,7 +35,7 @@ module Alephant
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def fetch_object(component_meta)
|
38
|
-
cache.get(component_meta.
|
38
|
+
cache.get(component_meta.component_key) { content component_meta }
|
39
39
|
end
|
40
40
|
|
41
41
|
def content(component_meta)
|
@@ -52,7 +52,6 @@ module Alephant
|
|
52
52
|
|
53
53
|
def request(component_meta)
|
54
54
|
before = Time.new
|
55
|
-
component_meta.cached = false
|
56
55
|
|
57
56
|
Faraday.get(url_for(component_meta)).tap do |r|
|
58
57
|
unless r.success?
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'alephant/broker/errors'
|
2
|
+
|
3
|
+
module Alephant
|
4
|
+
module Broker
|
5
|
+
module LoadStrategy
|
6
|
+
module Revalidate
|
7
|
+
class Fetcher
|
8
|
+
include Logger
|
9
|
+
|
10
|
+
attr_reader :component_meta
|
11
|
+
|
12
|
+
def initialize(component_meta)
|
13
|
+
@component_meta = component_meta
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch
|
17
|
+
Alephant::Broker::Cache::CachedObject.new(s3.get(s3_path))
|
18
|
+
rescue AWS::S3::Errors::NoSuchKey, InvalidCacheKey => error
|
19
|
+
logger.info(event: 'ErrorCaught', method: "#{self.class}#fetch", error: error)
|
20
|
+
logger.metric('S3InvalidCacheKey')
|
21
|
+
raise Alephant::Broker::Errors::ContentNotFound
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def s3_path
|
27
|
+
lookup_read = lookup.read(component_meta.id, component_meta.options, 1)
|
28
|
+
|
29
|
+
raise InvalidCacheKey if lookup_read.location.nil?
|
30
|
+
|
31
|
+
lookup_read.location
|
32
|
+
end
|
33
|
+
|
34
|
+
def s3
|
35
|
+
@s3 ||= Alephant::Storage.new(
|
36
|
+
Broker.config[:s3_bucket_id],
|
37
|
+
Broker.config[:s3_object_path]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def lookup
|
42
|
+
@lookup ||= Alephant::Lookup.create(
|
43
|
+
Broker.config[:lookup_table_name],
|
44
|
+
Broker.config
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Alephant
|
2
|
+
module Broker
|
3
|
+
module LoadStrategy
|
4
|
+
module Revalidate
|
5
|
+
class Refresher
|
6
|
+
include Logger
|
7
|
+
|
8
|
+
INFLIGHT_CACHE_TTL = 120 # expire the inflight key after 2 minutes
|
9
|
+
|
10
|
+
attr_reader :component_meta
|
11
|
+
|
12
|
+
def initialize(component_meta)
|
13
|
+
@component_meta = component_meta
|
14
|
+
end
|
15
|
+
|
16
|
+
def refresh
|
17
|
+
inflight = cache.get(inflight_cache_key)
|
18
|
+
|
19
|
+
logger.info(event: 'Inflight?', cache_val: inflight, method: "#{self.class}#refresh")
|
20
|
+
|
21
|
+
return if inflight
|
22
|
+
|
23
|
+
logger.info(event: 'QueueMessage', message: message, method: "#{self.class}#refresh")
|
24
|
+
|
25
|
+
queue.send_message(message)
|
26
|
+
cache.set(inflight_cache_key, true, INFLIGHT_CACHE_TTL)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def message
|
32
|
+
::JSON.generate(id: component_meta.id,
|
33
|
+
batch_id: component_meta.batch_id,
|
34
|
+
options: component_meta.options,
|
35
|
+
timestamp: Time.now.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def queue
|
39
|
+
@queue ||= proc do
|
40
|
+
client = AWS::SQS.new
|
41
|
+
url = client.queues.url_for(Broker.config[:sqs_queue_name], queue_options)
|
42
|
+
|
43
|
+
client.queues[url]
|
44
|
+
end.call
|
45
|
+
end
|
46
|
+
|
47
|
+
def queue_options
|
48
|
+
opts = {}
|
49
|
+
opts[:queue_owner_aws_account_id] = aws_acc_id if aws_acc_id
|
50
|
+
|
51
|
+
logger.info(event: 'SQSQueueOptionsConfigured', options: opts, method: "#{self.class}#queue_options")
|
52
|
+
|
53
|
+
opts
|
54
|
+
end
|
55
|
+
|
56
|
+
def aws_acc_id
|
57
|
+
Broker.config[:aws_account_id]
|
58
|
+
end
|
59
|
+
|
60
|
+
def cache
|
61
|
+
@cache ||= Cache::Client.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def cache_key
|
65
|
+
component_meta.component_key
|
66
|
+
end
|
67
|
+
|
68
|
+
def inflight_cache_key
|
69
|
+
"inflight-#{cache_key}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|