glowworm 0.3.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 (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