glowworm 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +6 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +129 -0
- data/LICENSE +19 -0
- data/README.md +326 -0
- data/Rakefile +29 -0
- data/bin/basic_server_tester +311 -0
- data/bin/em_server/config.ru +2 -0
- data/bin/em_server/em_server.rb +19 -0
- data/bin/em_server_tester +68 -0
- data/bin/glowworm +90 -0
- data/bin/load_tester +84 -0
- data/ci_jobs/glowworm-continuous-deploy-next-staging/run.sh +10 -0
- data/ci_jobs/glowworm-integrations/run.sh +15 -0
- data/ci_jobs/glowworm-performance/run.sh +2 -0
- data/ci_jobs/glowworm-robustness/run.sh +2 -0
- data/ci_jobs/glowworm-units/run.sh +13 -0
- data/ci_jobs/setup.sh +119 -0
- data/example/example_server.ecology +6 -0
- data/example/example_server.rb +32 -0
- data/glowworm.gemspec +54 -0
- data/lib/glowworm.rb +501 -0
- data/lib/glowworm/em.rb +8 -0
- data/lib/glowworm/no_bg.rb +2 -0
- data/lib/glowworm/version.rb +3 -0
- data/server/Gemfile +27 -0
- data/server/Gemfile.lock +87 -0
- data/server/PROTOCOL +39 -0
- data/server/check_mk_checks/check_glowworm_server +43 -0
- data/server/db_migrations/20111004214649_change_feature_accounts_to_string.rb +60 -0
- data/server/db_migrations/20111028104546_add_value_to_account_set_features.rb +12 -0
- data/server/db_migrations/20120217090636_add_fully_active_flag_to_features.rb +15 -0
- data/server/example_test_data.rb +66 -0
- data/server/glowworm_server.ecology.erb +16 -0
- data/server/glowworm_server.rb +226 -0
- data/server/run/server.sh +7 -0
- data/server/server_test.rb +72 -0
- data/server/version.rb +3 -0
- data/test/integration/basic_server_test.rb +90 -0
- data/test/integration/create_sqlite_data.rb +196 -0
- data/test/integration/em_server_test.rb +68 -0
- data/test/integration/gemfile_for_specific_glowworm_version +17 -0
- data/test/integration/gemfile_for_specific_glowworm_version.lock +55 -0
- data/test/integration/integration_test_helper.rb +153 -0
- data/test/integration/load_test.rb +59 -0
- data/test/integration/nginx.conf +23 -0
- data/test/integration/server_test.ecology.erb +6 -0
- data/test/test_helper.rb +47 -0
- data/test/units/em_test.rb +41 -0
- data/test/units/feature_flag_test.rb +297 -0
- data/test/units/no_bg_test.rb +40 -0
- data/test/units/request_test.rb +51 -0
- metadata +410 -0
data/lib/glowworm/em.rb
ADDED
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
|
data/server/Gemfile.lock
ADDED
@@ -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
|