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.
- 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
|