glowworm 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +6 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +129 -0
  6. data/LICENSE +19 -0
  7. data/README.md +326 -0
  8. data/Rakefile +29 -0
  9. data/bin/basic_server_tester +311 -0
  10. data/bin/em_server/config.ru +2 -0
  11. data/bin/em_server/em_server.rb +19 -0
  12. data/bin/em_server_tester +68 -0
  13. data/bin/glowworm +90 -0
  14. data/bin/load_tester +84 -0
  15. data/ci_jobs/glowworm-continuous-deploy-next-staging/run.sh +10 -0
  16. data/ci_jobs/glowworm-integrations/run.sh +15 -0
  17. data/ci_jobs/glowworm-performance/run.sh +2 -0
  18. data/ci_jobs/glowworm-robustness/run.sh +2 -0
  19. data/ci_jobs/glowworm-units/run.sh +13 -0
  20. data/ci_jobs/setup.sh +119 -0
  21. data/example/example_server.ecology +6 -0
  22. data/example/example_server.rb +32 -0
  23. data/glowworm.gemspec +54 -0
  24. data/lib/glowworm.rb +501 -0
  25. data/lib/glowworm/em.rb +8 -0
  26. data/lib/glowworm/no_bg.rb +2 -0
  27. data/lib/glowworm/version.rb +3 -0
  28. data/server/Gemfile +27 -0
  29. data/server/Gemfile.lock +87 -0
  30. data/server/PROTOCOL +39 -0
  31. data/server/check_mk_checks/check_glowworm_server +43 -0
  32. data/server/db_migrations/20111004214649_change_feature_accounts_to_string.rb +60 -0
  33. data/server/db_migrations/20111028104546_add_value_to_account_set_features.rb +12 -0
  34. data/server/db_migrations/20120217090636_add_fully_active_flag_to_features.rb +15 -0
  35. data/server/example_test_data.rb +66 -0
  36. data/server/glowworm_server.ecology.erb +16 -0
  37. data/server/glowworm_server.rb +226 -0
  38. data/server/run/server.sh +7 -0
  39. data/server/server_test.rb +72 -0
  40. data/server/version.rb +3 -0
  41. data/test/integration/basic_server_test.rb +90 -0
  42. data/test/integration/create_sqlite_data.rb +196 -0
  43. data/test/integration/em_server_test.rb +68 -0
  44. data/test/integration/gemfile_for_specific_glowworm_version +17 -0
  45. data/test/integration/gemfile_for_specific_glowworm_version.lock +55 -0
  46. data/test/integration/integration_test_helper.rb +153 -0
  47. data/test/integration/load_test.rb +59 -0
  48. data/test/integration/nginx.conf +23 -0
  49. data/test/integration/server_test.ecology.erb +6 -0
  50. data/test/test_helper.rb +47 -0
  51. data/test/units/em_test.rb +41 -0
  52. data/test/units/feature_flag_test.rb +297 -0
  53. data/test/units/no_bg_test.rb +40 -0
  54. data/test/units/request_test.rb +51 -0
  55. metadata +410 -0
@@ -0,0 +1,8 @@
1
+ begin
2
+ require "em-synchrony"
3
+ require "em-net-http"
4
+ rescue LoadError
5
+ raise "You must have em/sinatra-synchrony AND em-net-http installed for the glowworm/em to work"
6
+ end
7
+ require "glowworm"
8
+ Glowworm.send(:_em_)
@@ -0,0 +1,2 @@
1
+ require "glowworm"
2
+ Glowworm.no_bg
@@ -0,0 +1,3 @@
1
+ module Glowworm
2
+ VERSION = "0.3.0"
3
+ end
data/server/Gemfile ADDED
@@ -0,0 +1,27 @@
1
+ source "http://rubygems.org"
2
+ source "http://gems.ooyala.com"
3
+
4
+ gem "capistrano"
5
+ gem "ecology", "~>0.0.14"
6
+ gem "multi_json", "~>1.3.2"
7
+ gem "mysql" # Should be mysql2, but sequel doesn't support it right now
8
+ gem "railsless-deploy", :require => nil # see https://github.com/leehambley/railsless-deploy#bundler-usage
9
+ gem "sequel"
10
+ gem "termite", '~>0.0.14'
11
+ gem "yajl-ruby" # This is much faster than MultiJson's default OKJson
12
+
13
+ group :deploy do
14
+ gem "email-utils"
15
+ end
16
+
17
+ group :test do
18
+ gem "sqlite3"
19
+ gem "trollop"
20
+ gem "glowworm"
21
+ gem "minitap", ">= 0.3.5"
22
+ end
23
+
24
+ group :development do
25
+ gem "pry"
26
+ gem "httparty"
27
+ end
@@ -0,0 +1,87 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ remote: http://gems.ooyala.com/
4
+ specs:
5
+ ansi (1.4.3)
6
+ capistrano (2.14.1)
7
+ highline
8
+ net-scp (>= 1.0.0)
9
+ net-sftp (>= 2.0.0)
10
+ net-ssh (>= 2.0.14)
11
+ net-ssh-gateway (>= 1.1.0)
12
+ chronic (0.9.0)
13
+ coderay (1.0.8)
14
+ ecology (0.0.18)
15
+ erubis
16
+ multi_json
17
+ email-utils (0.0.2)
18
+ erubis (2.7.0)
19
+ glowworm (0.1.8)
20
+ ecology (~> 0.0.18)
21
+ hastur (~> 1.2.0)
22
+ httparty
23
+ multi_json
24
+ termite (~> 0.0.13)
25
+ trollop
26
+ hastur (1.2.18)
27
+ chronic
28
+ multi_json (~> 1.3.2)
29
+ highline (1.6.15)
30
+ httparty (0.10.0)
31
+ multi_json (~> 1.0)
32
+ multi_xml
33
+ json (1.7.6)
34
+ method_source (0.8.1)
35
+ minitap (0.3.5)
36
+ minitest
37
+ tapout (>= 0.3.0)
38
+ minitest (4.4.0)
39
+ multi_json (1.3.7)
40
+ multi_xml (0.5.2)
41
+ mysql (2.9.0)
42
+ net-scp (1.0.4)
43
+ net-ssh (>= 1.99.1)
44
+ net-sftp (2.0.5)
45
+ net-ssh (>= 2.0.9)
46
+ net-ssh (2.6.3)
47
+ net-ssh-gateway (1.1.0)
48
+ net-ssh (>= 1.99.1)
49
+ pry (0.9.10)
50
+ coderay (~> 1.0.5)
51
+ method_source (~> 0.8)
52
+ slop (~> 3.3.1)
53
+ railsless-deploy (1.0.2)
54
+ rainbow (1.1.4)
55
+ sequel (3.43.0)
56
+ slop (3.3.3)
57
+ sqlite3 (1.3.7)
58
+ tapout (0.4.1)
59
+ ansi
60
+ json
61
+ termite (0.0.20)
62
+ ecology (~> 0.0.6)
63
+ multi_json
64
+ rainbow (~> 1.1.3)
65
+ trollop (2.0)
66
+ yajl-ruby (1.1.0)
67
+
68
+ PLATFORMS
69
+ ruby
70
+
71
+ DEPENDENCIES
72
+ capistrano
73
+ ecology (~> 0.0.14)
74
+ email-utils
75
+ glowworm
76
+ hastur (~> 1.2.0)
77
+ httparty
78
+ minitap (>= 0.3.5)
79
+ multi_json (~> 1.3.2)
80
+ mysql
81
+ pry
82
+ railsless-deploy
83
+ sequel
84
+ sqlite3
85
+ termite (~> 0.0.14)
86
+ trollop
87
+ yajl-ruby
data/server/PROTOCOL ADDED
@@ -0,0 +1,39 @@
1
+ Glowworm's wire protocol is intentionally very simple, and carried via
2
+ HTTP requests. Some goals, in order of importance:
3
+
4
+ * Simplicity, in order to achieve high Reliability and Availability
5
+ * Readability and Debuggability
6
+ * Not too unreasonably high bandwidth
7
+
8
+ A given request will be a read of a set of accounts and a set of
9
+ features. Usually, that is one of:
10
+
11
+ * One feature for one account
12
+ * One feature for all accounts
13
+ * All features for one account
14
+ * All features for all accounts
15
+
16
+ A request will return data in the following form:
17
+
18
+ * What account_sets the given account(s) are part of
19
+ * The setting of each feature for each account_set
20
+ * A list of override flags for specific account/feature combinations
21
+
22
+ That data is serialized as JSON, in the following form:
23
+
24
+ {
25
+ "version": 1,
26
+ "account_sets": {
27
+ "13742": 1, // account 13742 is part of account_set 1
28
+ "19244": [7,8], // account 19244 is part of account_sets 7 and 8
29
+ //...
30
+ },
31
+ "features": {
32
+ "turn_foo_widget_orange": [1, 2, 3, 4, 6, 7, 8], // account_sets for which this feature is active by default
33
+ //...
34
+ },
35
+ "overrides": [
36
+ "turn_foo_widget_orange": {"19244": false, "742": true},
37
+ //...
38
+ ]
39
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ require "time"
3
+
4
+ SERVICE_NAME="GlowwormServerStatus"
5
+ HOST="127.0.0.1"
6
+ PORT=4999
7
+ URI="/healthz"
8
+ URL="http://#{HOST}:#{PORT}#{URI}"
9
+
10
+ OUTPUT=`curl -I #{URL} 2> /dev/null`
11
+ if OUTPUT.empty?
12
+ puts "2 #{SERVICE_NAME} - Glowworm Nginx Server Down"
13
+ exit
14
+ end
15
+
16
+ MOD_TIME=Time.parse `echo "#{OUTPUT}" |grep Last-Modified |sed -e 's/^Last-Modified: //'`
17
+ HTTP_STATUS=OUTPUT.split[1].to_i
18
+
19
+ # TODO(jbhat): We need to check that output's time, which looks like: Thu, 18 Oct 2012 05:56:57 GMT,
20
+ # is within 10 minutes of now. If we get a 404, we should return that the glowworm server is down.
21
+ # If we get a 500, we should report that there is an error (shouldn't happen).
22
+ # If we get a 200, but the file is more than 10 minutes ago, we should return an error of stale data.
23
+ # If we get a 200 with fresh file, but the contents are not OOYALA GLOWWORM OK, we should error its contents.
24
+ # Finally, we can return ok if the file has last been modified within 10 minutes, and contains the string.
25
+ STATUS, OUTPUT_TEXT = case HTTP_STATUS
26
+ when 200
27
+ AGE=Time.now - MOD_TIME
28
+ if(AGE > 600)
29
+ [2, "Data Stale, healthz has not been updated in #{AGE} seconds."]
30
+ else
31
+ HEALTHZ_CONTENTS=`curl #{URL} 2> /dev/null`.chomp
32
+ if HEALTHZ_CONTENTS =~ /OOYALA GLOWWORM OK/
33
+ [0, HEALTHZ_CONTENTS]
34
+ else
35
+ [2, HEALTHZ_CONTENTS]
36
+ end
37
+ end
38
+ when 404
39
+ [2, "404 Glowworm Service Down"]
40
+ else
41
+ [2, "#{STATUS} Error Retrieving Healthz"]
42
+ end
43
+ puts "#{STATUS} #{SERVICE_NAME} - #{OUTPUT_TEXT}"
@@ -0,0 +1,60 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table(:account_sets) do
4
+ primary_key :id
5
+ Datetime :created_at, :null => false
6
+ Datetime :updated_at, :null => false
7
+ String :name, :null => false
8
+ String :description
9
+ index :name, :unique => true
10
+ end
11
+
12
+ create_table(:account_set_accounts) do
13
+ primary_key :id
14
+ Datetime :added_at, :null => false
15
+ Integer :account_set_id, :null => false
16
+ Integer :provider_id, :null => true
17
+ String :account, :limit => 10, :null => false
18
+ index :account_set_id
19
+ index :provider_id
20
+ index :account
21
+ index [:account_set_id, :account], :unique => true
22
+ end
23
+
24
+ create_table(:features) do
25
+ primary_key :id
26
+ Datetime :created_at, :null => false
27
+ Datetime :updated_at, :null => false
28
+ String :name, :null => false
29
+ String :description
30
+ index :name, :unique => true
31
+ end
32
+
33
+ create_table(:account_set_features) do
34
+ primary_key :id
35
+ Datetime :added_at, :null => false
36
+ Integer :account_set_id, :null => false
37
+ Integer :feature_id, :null => false
38
+ index :feature_id
39
+ index [:account_set_id, :feature_id], :unique => true
40
+ end
41
+
42
+ create_table(:feature_account_overrides) do
43
+ Integer :provider_id, :null => true
44
+ String :account, :limit => 10, :null => false
45
+ Integer :feature_id, :null => false
46
+ Integer :value, :null => false
47
+ index :provider_id
48
+ index :feature_id
49
+ index [:account, :feature_id], :unique => true
50
+ end
51
+ end
52
+
53
+ down do
54
+ drop_table :account_sets
55
+ drop_table :account_set_accounts
56
+ drop_table :features
57
+ drop_table :account_set_features
58
+ drop_table :feature_account_overrides
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ require File.join(File.dirname(__FILE__), "migration_helper")
2
+
3
+ Sequel.migration do
4
+ up do
5
+ add_column :account_set_features, :value, Integer, :null => false, :default => 1
6
+ self[:account_set_features].update(:value => 1)
7
+ end
8
+
9
+ down do
10
+ drop_column :account_set_features, :value
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), "migration_helper")
2
+
3
+ Sequel.migration do
4
+ up do
5
+ alter_table(:features) do
6
+ add_column :fully_active, :boolean, :default => false
7
+ end
8
+ end
9
+
10
+ down do
11
+ alter_table(:features) do
12
+ drop_column :fully_active
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ecology"
4
+ require "sequel"
5
+ require "termite"
6
+
7
+ Ecology.read("glowworm_server.ecology")
8
+
9
+ db_spec = Ecology.property("db") || "sqlite:glowworm_server.sqlite"
10
+ DB = Sequel.connect db_spec
11
+ MyLogger = Termite::Logger.new
12
+
13
+ if ARGV.include?("--clear")
14
+ DB[:account_sets].delete
15
+ DB[:features].delete
16
+ DB[:account_set_accounts].delete
17
+ DB[:account_set_features].delete
18
+ DB[:feature_account_overrides].delete
19
+ end
20
+
21
+ DB[:account_sets].insert(:name => "Major", :description => "Major accounts", :id => 1,
22
+ :created_at => Time.now, :updated_at => Time.now)
23
+ DB[:account_sets].insert(:name => "Minor", :description => "Minor accounts", :id => 2,
24
+ :created_at => Time.now, :updated_at => Time.now)
25
+
26
+
27
+ DB[:account_set_accounts].insert(:account_set_id => 1, :account => "27366", :provider_id => 27366,
28
+ :added_at => Time.now)
29
+
30
+ # Add account #35 to major accounts
31
+ DB[:account_set_accounts].insert(:account_set_id => 1, :account => "35", :provider_id => 35,
32
+ :added_at => Time.now)
33
+
34
+ # Add account #42 to minor accounts
35
+ DB[:account_set_accounts].insert(:account_set_id => 2, :account => "42", :provider_id => 42,
36
+ :added_at => Time.now)
37
+
38
+ # Add account #49 to minor accounts
39
+ DB[:account_set_accounts].insert(:account_set_id => 2, :account => "49", :provider_id => 49,
40
+ :added_at => Time.now)
41
+
42
+ DB[:features].insert(:id => 100, :name => "video_rec", :description => "video_rec",
43
+ :created_at => Time.now, :updated_at => Time.now)
44
+
45
+
46
+ DB[:features].insert(:id => 1, :name => "foo_feature", :description => "foo",
47
+ :created_at => Time.now, :updated_at => Time.now)
48
+
49
+ DB[:features].insert(:id => 2, :name => "new_signup_message", :description => "Use new signup message?",
50
+ :created_at => Time.now, :updated_at => Time.now)
51
+
52
+ # Turn on foo_feature for major accounts, but not minor
53
+ DB[:account_set_features].insert(:account_set_id => 1, :feature_id => 1,
54
+ :added_at => Time.now)
55
+ DB[:account_set_features].insert(:account_set_id => 1, :feature_id => 100,
56
+ :added_at => Time.now)
57
+
58
+ if ARGV.include?("--new-signup")
59
+ DB[:account_set_features].insert(:account_set_id => 1, :feature_id => 2,
60
+ :added_at => Time.now)
61
+ end
62
+
63
+ DB[:feature_account_overrides].insert(:account => "49", :provider_id => 49, :feature_id => 1,
64
+ :value => 1)
65
+
66
+ puts "Added test data successfully!"
@@ -0,0 +1,16 @@
1
+ <%
2
+ begin
3
+ require 'config/pushed_environment'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
8
+ db_user = (DB_USER rescue "root")
9
+ db_password = (DB_PASSWORD rescue "")
10
+ db_location = (DB_LOCATION rescue "localhost")
11
+ %>
12
+ {
13
+ "application": "glowworm_server",
14
+ "db": "mysql://<%= db_user %>:<%= db_password %>@<%= db_location %>/vstreams",
15
+ "cache_period": 300
16
+ }
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "sequel"
4
+ require "yajl"
5
+ require "multi_json"
6
+ require "ecology"
7
+ require "termite"
8
+ require "mysql"
9
+
10
+ require_relative "config/pushed_environment"
11
+ require "digest/md5"
12
+ require "fileutils"
13
+
14
+ $LOAD_PATH << File.dirname(__FILE__)
15
+ # Try to load the symlinked Glowworm version file, if it rsync'd
16
+ (require "version") rescue nil
17
+
18
+ GLOWWORM_VERSION = (Glowworm::VERSION rescue "Unknown")
19
+
20
+ # Be explicit about engine
21
+ MultiJson.use :yajl
22
+
23
+ # Calculate the md5 from the data passed in. Assumes a hash with no md5 unless told otherwise
24
+ def md5_from_hash(json_hash, options = { :json => false, :md5 => false })
25
+ if options[:json]
26
+ json_hash = MultiJson.load json_hash rescue {} # Assume empty hash if there is a problem
27
+ end
28
+
29
+ json_hash.delete "md5" if options[:md5]
30
+
31
+ dumped_json = MultiJson.dump json_hash
32
+ md5 = Digest::MD5.hexdigest(dumped_json)
33
+ md5
34
+ end
35
+
36
+ # accounts - hash of account names to account set IDs
37
+ # features - hash of feature names to arrays of account sets that set it
38
+ # overrides - hash of feature names to hashes of {account => value}
39
+ def format_json(accounts, features, overrides, fully_active)
40
+ json_hash = {
41
+ "version" => 1,
42
+ "account_sets" => accounts,
43
+ "features" => features,
44
+ "overrides" => overrides,
45
+ "fully_active" => fully_active,
46
+ }
47
+
48
+ return json_hash
49
+ end
50
+
51
+ class DBException < Exception
52
+ end
53
+
54
+ def try_db
55
+ yield
56
+ rescue => e
57
+ MyLogger.error "Error talking to database. Message: #{e.message}. Backtrace: #{e.backtrace}"
58
+ raise DBException
59
+ end
60
+
61
+ def json_for_account_and_feature
62
+ MyLogger.info "Querying DB for all accounts and features"
63
+
64
+ # If there has previously been a DB failure, try to reconnect right away
65
+ try_db do
66
+ DB.connect DB_SPEC
67
+ @failures = 0
68
+ end if @failures > 0
69
+
70
+ # Right now, account_sets with no accounts won't be returned. I think that's fine.
71
+ accounts = {}
72
+ try_db do
73
+ DB[:account_set_accounts].each do |row|
74
+ accounts[row[:account]] ||= []
75
+ accounts[row[:account]] << row[:account_set_id].to_s
76
+ end
77
+ end
78
+
79
+ features = {}
80
+ fully_active = {}
81
+ try_db do
82
+ DB[:features].left_join(:account_set_features, :feature_id => :id).each do |row|
83
+ features[row[:name]] ||= {}
84
+ features[row[:name]][row[:account_set_id].to_s] = row[:value] if row[:account_set_id]
85
+ fully_active[row[:name]] = row[:fully_active]
86
+ end
87
+ end
88
+
89
+ overrides = {}
90
+ try_db do
91
+ DB[:feature_account_overrides].join(:features, :id => :feature_id).each do |row|
92
+ overrides[row[:name]] ||= {}
93
+ overrides[row[:name]][row[:account]] = row[:value]
94
+ end
95
+ end
96
+
97
+ json = format_json accounts, features, overrides, fully_active
98
+
99
+ MyLogger.debug "Returning JSON: #{json}"
100
+
101
+ [200, json]
102
+ rescue DBException
103
+ [500, ""]
104
+ end
105
+
106
+ Ecology.read("glowworm_server.ecology")
107
+ SERVING_DIRECTORY = Ecology.property("nginx_directory") || "#{SHARED_PATH}/www"
108
+ LIVE_FILENAME = "#{SHARED_PATH}/server_is_live" unless defined? LIVE_FILENAME
109
+ FileUtils.mkdir_p SERVING_DIRECTORY
110
+ AIM_FILE = ENV['AIM_FILE'] || "#{SERVING_DIRECTORY}/all_if_modified"
111
+ STATUSZ_FILE = "#{SERVING_DIRECTORY}/statusz"
112
+ HEALTHZ_FILE = "#{SERVING_DIRECTORY}/healthz"
113
+ DB_SPEC = Ecology.property("db") || "sqlite:glowworm_server.sqlite"
114
+ cache_period = Ecology.property("cache_period") || 60
115
+ MyLogger = Termite::Logger.new nil, nil, nil, :use_logger_prefix => true, :stderr_level => "info"
116
+ @failures = 0
117
+ begin
118
+ DB = try_db { Sequel.connect DB_SPEC }
119
+ rescue DBException
120
+ @failures += 1
121
+ end
122
+ md5_hash = if File.exists? AIM_FILE
123
+ contents = File.open(AIM_FILE).read
124
+ md5_from_hash(contents, :json => true, :md5 => true)
125
+ else
126
+ nil
127
+ end
128
+
129
+ def write_statusz_file(status)
130
+ str = "Glowworm server from gem version '#{GLOWWORM_VERSION}' Running on Ruby #{RUBY_VERSION}.\n"
131
+ str += if status == 200 && @failures.zero?
132
+ "Last modified #{@last_modified || "in previous run period"}."
133
+ else
134
+ "Last update failed due to database connection error at #{Time.now}.\n" +
135
+ "#{@failures} consecutive connection errors encountered."
136
+ end
137
+ File.open(STATUSZ_FILE, "w") do |file|
138
+ file.puts str
139
+ end
140
+ end
141
+
142
+ def write_healthz_file(status)
143
+ # Return 404 if LIVE_FILENAME is not present
144
+ if !File.exists?(LIVE_FILENAME)
145
+ File.delete HEALTHZ_FILE if File.exists?(HEALTHZ_FILE)
146
+ return
147
+ end
148
+
149
+ str = if status == 200 && @failures.zero?
150
+ "OOYALA GLOWWORM OK"
151
+ else
152
+ "Error reconnecting to database. #{@failures} consecutive connection errors encountered."
153
+ end
154
+ File.open(HEALTHZ_FILE, "w") do |file|
155
+ file.puts str
156
+ end
157
+ end
158
+
159
+ unless File.exists? STATUSZ_FILE
160
+ # Write statusz file. Parameter is status code, but will output the right thing unless passed 500.
161
+ write_statusz_file nil
162
+ end
163
+
164
+ if File.exists?(HEALTHZ_FILE) && !File.exists?(LIVE_FILENAME)
165
+ # Write statusz file. Parameter is status code, but will output the right thing unless passed 500
166
+ write_healthz_file nil
167
+ end
168
+
169
+ # This is an implementation of "do while": run once if ENV["RUN_ONCE"] is set, loop otherwise
170
+ run_repeatedly = !ENV.has_key?("RUN_ONCE")
171
+ begin
172
+ status, json_hash = json_for_account_and_feature
173
+
174
+ new_md5_hash = md5_from_hash(json_hash)
175
+
176
+ if status == 200 && @failures.zero?
177
+ @failures = 0
178
+ @last_success = Time.now
179
+ if md5_hash != new_md5_hash || !File.exists?(AIM_FILE)
180
+ @last_modified = Time.now
181
+
182
+ json_hash[:md5] = new_md5_hash
183
+ # Yes, this is reencoding all the JSON. We *could* just do hideous string
184
+ # surgery and replace the final close-brace with a comma, the MD5 bit, and
185
+ # a new close-brace. For now, clarity beats efficiency since this runs rarely.
186
+ json_string = MultiJson.dump(json_hash)
187
+ # If we get to this point, then the file should have been modified, otherwise the hashes would match
188
+ File.open(AIM_FILE, "w") do |file|
189
+ file.write json_string
190
+ end
191
+ md5_hash = new_md5_hash
192
+ MyLogger.info "NEW_DATA - #{json_string[0..40]}..., hash #{md5_hash}"
193
+ else
194
+ MyLogger.info "No new data - checksum #{md5_hash}"
195
+ end
196
+ else # status == 500
197
+ @failures += 1
198
+ @retry_time = case @failures
199
+ when 0..1
200
+ 0 # retry immediately on first failure
201
+ when 2..7
202
+ 2 ** (@failures - 2) # 1, 2, 4, 8, 16, 32
203
+ else
204
+ 55 # sleep for 55 seconds after 7th failure (because cmk retry is 60 seconds)
205
+ end
206
+ MyLogger.error "#{@failures} consecutive database failure(s). Retrying in #{@retry_time} seconds."
207
+ end
208
+ write_statusz_file status
209
+ write_healthz_file status if status != 500 || (Time.now - @last_success) > 60 * 10
210
+ if run_repeatedly
211
+ # Randomize the cache period so we don't get a thundering herd of
212
+ # Glowworm servers all updating on the same five-minute
213
+ # boundary every five minutes. This is a big DB query.
214
+ #
215
+ # We continue to randomize as we go forward instead of starting
216
+ # with a random delay. This is because delays in service will
217
+ # otherwise tend to clump servers up over time, causing an
218
+ # emergent thundering herd.
219
+ sleep_period = if status == 500
220
+ @retry_time
221
+ else
222
+ (cache_period / 2) + Random.rand(cache_period + 1)
223
+ end
224
+ sleep sleep_period
225
+ end
226
+ end while run_repeatedly