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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +22 -9
  3. data/README.md +35 -6
  4. data/alephant-broker.gemspec +3 -2
  5. data/lib/alephant/broker/cache.rb +1 -0
  6. data/lib/alephant/broker/cache/cached_object.rb +71 -0
  7. data/lib/alephant/broker/cache/client.rb +29 -17
  8. data/lib/alephant/broker/cache/null_client.rb +12 -4
  9. data/lib/alephant/broker/component.rb +3 -1
  10. data/lib/alephant/broker/component_meta.rb +2 -0
  11. data/lib/alephant/broker/error_component.rb +1 -1
  12. data/lib/alephant/broker/load_strategy.rb +1 -0
  13. data/lib/alephant/broker/load_strategy/http.rb +2 -3
  14. data/lib/alephant/broker/load_strategy/revalidate/fetcher.rb +51 -0
  15. data/lib/alephant/broker/load_strategy/revalidate/refresher.rb +75 -0
  16. data/lib/alephant/broker/load_strategy/revalidate/strategy.rb +86 -0
  17. data/lib/alephant/broker/load_strategy/s3/base.rb +2 -2
  18. data/lib/alephant/broker/response/asset.rb +1 -1
  19. data/lib/alephant/broker/response/base.rb +7 -8
  20. data/lib/alephant/broker/response/batch.rb +1 -1
  21. data/lib/alephant/broker/version.rb +1 -1
  22. data/spec/alephant/broker/cache/cached_object_spec.rb +107 -0
  23. data/spec/alephant/broker/load_strategy/http_spec.rb +5 -7
  24. data/spec/alephant/broker/load_strategy/revalidate/fetcher_spec.rb +88 -0
  25. data/spec/alephant/broker/load_strategy/revalidate/refresher_spec.rb +69 -0
  26. data/spec/alephant/broker/load_strategy/revalidate/strategy_spec.rb +185 -0
  27. data/spec/fixtures/json/batch_compiled_no_sequence.json +1 -0
  28. data/spec/integration/not_modified_response_spec.rb +4 -6
  29. data/spec/integration/rack_spec.rb +0 -3
  30. data/spec/integration/revalidate_spec.rb +174 -0
  31. data/spec/spec_helper.rb +1 -0
  32. metadata +99 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a1de9bc629522c3ada2ac0f2ad0c08aa5024a037
4
- data.tar.gz: b43733742699f3a88442386e88c4e77c3c782373
3
+ metadata.gz: 18fa7932242b113184d3dd00fcea6fd0700d6bd8
4
+ data.tar.gz: 4b8c2034dfe9275d18aa1163bc65bd47d8463cb8
5
5
  SHA512:
6
- metadata.gz: 5d36de810da755b2210767bdeaf5e1d0230d65e1f43aea5266e19d569665f3b51255200fdb4ab14eafa925735f9b18b43f92c4403e20993b016cf9f4f68f64e3
7
- data.tar.gz: 1ff51c8a99183673ba61538b20d9d12c6f1ee9d89091a88057bdad4d8d6c38230cf252242fc4569ea457c73d9cbe76d2ba3f24d5c4e411cdd2cc297de2008901
6
+ metadata.gz: 3183c49f2222401e560b929fed1887e3b67c169bff1b33764489955a7cf14fcb144d40d79e9006cb1f42d282989ee1cafec562bfebb81845a42577f3c76df24e
7
+ data.tar.gz: 5fe75198d006255e0573c4ac4fec30e49b622e75f4059ea1089edcc53fa7f61cfebd12b7c79ab9363b59ba25f61f9b31b7900e6be28457351399a518c4831b89
@@ -1,11 +1,24 @@
1
+ sudo: false
2
+ cache: bundler
1
3
  language: ruby
4
+
5
+ # Ruby build matrix
2
6
  rvm:
3
- - "jruby-1.7.16"
4
- notifications:
5
- email:
6
- recipients:
7
- - FutureMediaNewsRubyGems@bbc.co.uk
8
- on_failure: change
9
- on_success: never
10
- sudo: false
11
- script: bundle exec rake all
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/load_strategy/http'
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
+ ![https://raw.githubusercontent.com/BBC-News/Documentation/master/Alephant/Diagrams/Alephant%20Revalidate%20Broker.png?token=AADB7YskdW22P6z-hDjfk12UiR7bZA8Jks5XkI7EwA%3D%3D](https://raw.githubusercontent.com/BBC-News/Documentation/master/Alephant/Diagrams/Alephant%20Revalidate%20Broker.png?token=AADB7YskdW22P6z-hDjfk12UiR7bZA8Jks5XkI7EwA%3D%3D)
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
-
@@ -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'
@@ -3,6 +3,7 @@ module Alephant
3
3
  module Cache
4
4
  require "alephant/broker/cache/client"
5
5
  require "alephant/broker/cache/null_client"
6
+ require "alephant/broker/cache/cached_object"
6
7
  end
7
8
  end
8
9
  end
@@ -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 "dalli-elasticache"
2
- require "alephant/logger"
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 "Broker::Cache::Client#initialize: No config endpoint, NullClient used"
15
- logger.metric "NoConfigEndpoint"
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, :expires_in => ttl)
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 key
25
- result = @client.get versioned_key
26
- logger.info "Broker::Cache::Client#get key: #{versioned_key} - #{result ? 'hit' : 'miss'}"
27
- logger.metric "GetKeyMiss" unless result
28
- result ? result : set(key, block.call)
29
- rescue StandardError => e
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, ttl = nil)
34
- value.tap { |o| @client.set(versioned(key), o, ttl) }
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["elasticache_config_endpoint"]
52
+ Broker.config[:elasticache_config_endpoint] || Broker.config['elasticache_config_endpoint']
41
53
  end
42
54
 
43
55
  def ttl
44
- Broker.config["elasticache_ttl"] || DEFAULT_TTL
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["elasticache_cache_version"]
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 get(_key)
6
- yield
5
+ def initialize
6
+ @cache = {}
7
7
  end
8
8
 
9
- def set(_key, value, _ttl = nil)
10
- value
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
@@ -35,7 +35,9 @@ module Alephant
35
35
  end
36
36
 
37
37
  def status
38
- data[:meta].key?("status") ? data[:meta]["status"] : 200
38
+ meta = data.fetch(:meta, {})
39
+
40
+ meta[:status] || meta['status'] || 200
39
41
  end
40
42
 
41
43
  private
@@ -17,6 +17,8 @@ module Alephant
17
17
  Crimp.signature options
18
18
  end
19
19
 
20
+ # NOTE: This is in use in `alephant-publisher-queue` also, so if you
21
+ # change this, you'll need to change this there also.
20
22
  def component_key
21
23
  "#{id}/#{opts_hash}"
22
24
  end
@@ -30,7 +30,7 @@ module Alephant
30
30
  end
31
31
 
32
32
  def debug?
33
- Broker.config.fetch("debug", false)
33
+ Broker.config[:debug] || Broker.config["debug"] || false
34
34
  end
35
35
  end
36
36
  end
@@ -4,6 +4,7 @@ module Alephant
4
4
  require "alephant/broker/load_strategy/http"
5
5
  require "alephant/broker/load_strategy/s3/archived"
6
6
  require "alephant/broker/load_strategy/s3/sequenced"
7
+ require "alephant/broker/load_strategy/revalidate/strategy"
7
8
  end
8
9
  end
9
10
  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.cache_key, content(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.cache_key) { content 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