alephant-broker 3.14.0 → 3.15.0

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