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/bin/load_tester
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "glowworm"
|
4
|
+
require "trollop"
|
5
|
+
|
6
|
+
OPTS = Trollop::options do
|
7
|
+
opt :concurrent, "How many threads", :type => :int, :default => 1
|
8
|
+
opt :runs, "How many times to run", :type => :int, :default => 1
|
9
|
+
opt :account, "An account to query", :type => String, :required => true, :multi => true
|
10
|
+
opt :feature, "A feature to query", :type => String, :required => true, :multi => true
|
11
|
+
opt :server, "Server to query", :type => String, :default => "http://localhost:4999"
|
12
|
+
opt :fork, "Fork instead of threading", :type => :boolean, :default => false
|
13
|
+
opt :delay, "Seconds to sleep between requests (e.g. 0.001)", :type => :float, :default => 0.0
|
14
|
+
opt :header, "Print header information", :type => :boolean, :default => true
|
15
|
+
opt :log_too_much, "Log too much", :type => :boolean
|
16
|
+
opt :show_backtrace, "Show error traces", :type => :boolean
|
17
|
+
opt :timeout, "Timeout per request", :type => Float, :default => 0.1
|
18
|
+
end
|
19
|
+
|
20
|
+
if OPTS[:header]
|
21
|
+
puts "We print dot for correct responses, F for incorrect responses, and E for exceptions."
|
22
|
+
puts "#{OPTS[:concurrent]} #{OPTS[:fork] ? "processes" : "threads"}, #{OPTS[:server]}"
|
23
|
+
puts "Client version #{Glowworm::VERSION} go!"
|
24
|
+
end
|
25
|
+
|
26
|
+
CHAR_LIST = (0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a
|
27
|
+
def num_to_char(num)
|
28
|
+
n = num % 62
|
29
|
+
CHAR_LIST[n]
|
30
|
+
end
|
31
|
+
|
32
|
+
Glowworm.server = OPTS[:server]
|
33
|
+
|
34
|
+
if ENV["TEST_GLOWWORM_VERSION"] && ENV["TEST_GLOWWORM_VERSION"] != "latest" && Glowworm::VERSION != ENV["TEST_GLOWWORM_VERSION"]
|
35
|
+
fail "Should be testing under #{ENV["TEST_GLOWWORM_VERSION"]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
$stderr.puts "Glowworm version: #{Glowworm::VERSION}"
|
39
|
+
|
40
|
+
# Silence all logging
|
41
|
+
fake_logger = Object.new
|
42
|
+
def fake_logger.method_missing(*args); end
|
43
|
+
Glowworm.termite_logger = fake_logger
|
44
|
+
|
45
|
+
threads = []
|
46
|
+
forks = []
|
47
|
+
OPTS[:concurrent].times do
|
48
|
+
action = proc do
|
49
|
+
if OPTS[:log_too_much]
|
50
|
+
# TODO(noah): Use thread ID not PID for threads
|
51
|
+
log_path = "/tmp/gw_multi_test_#{Process.pid}_#{num_to_char(Process.pid)}"
|
52
|
+
logger = Termite::Logger.new(log_path, nil, nil, :level => "debug", :console_print => false)
|
53
|
+
Glowworm.termite_logger = logger
|
54
|
+
end
|
55
|
+
OPTS[:runs].times do
|
56
|
+
begin
|
57
|
+
x = Glowworm.feature_flag(OPTS[:account].sample, OPTS[:feature].sample, :default => false,
|
58
|
+
:timeout => OPTS[:timeout], :ttl => 0.0)
|
59
|
+
if x == true
|
60
|
+
print "."
|
61
|
+
else
|
62
|
+
print "F(#{num_to_char(Process.pid)})"
|
63
|
+
end
|
64
|
+
rescue
|
65
|
+
print "E"
|
66
|
+
puts "Backtrace: #{$!.message}\n #{$!.backtrace.join("\n")}" if OPTS[:show_backtrace]
|
67
|
+
end
|
68
|
+
sleep(OPTS[:delay]) if OPTS[:delay] > 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if OPTS[:fork]
|
73
|
+
forks << fork(&action)
|
74
|
+
else
|
75
|
+
threads << Thread.new(&action)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if OPTS[:fork]
|
80
|
+
forks.each { |f| Process.wait f }
|
81
|
+
else
|
82
|
+
threads.each{ |t| t.join }
|
83
|
+
end
|
84
|
+
puts ""
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
export PATH=~/.rbenv/bin/:${PATH}
|
4
|
+
eval "$(rbenv init - --no-rehash)"
|
5
|
+
|
6
|
+
export OO_DEPLOY_DIR=/home/role-c3/ooyala-security/deploy_configs
|
7
|
+
cd glowworm/server/
|
8
|
+
gem install --no-rdoc --no-ri bundler
|
9
|
+
bundle install --without test
|
10
|
+
bundle exec cap to_next_staging deploy:full
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash -l
|
2
|
+
|
3
|
+
if [[ -z $WORKSPACE ]] ; then
|
4
|
+
# Support execution from the shell
|
5
|
+
export PROJECT_DIR=$(pwd)
|
6
|
+
else
|
7
|
+
export PROJECT_DIR="$WORKSPACE/glowworm"
|
8
|
+
fi
|
9
|
+
|
10
|
+
source "$PROJECT_DIR/ci_jobs/setup.sh"
|
11
|
+
|
12
|
+
echo START TASK: tests
|
13
|
+
exec_rake_task test:integration
|
14
|
+
echo END TASK
|
15
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash -l
|
2
|
+
|
3
|
+
if [[ -z $WORKSPACE ]] ; then
|
4
|
+
# Support execution from the shell
|
5
|
+
export PROJECT_DIR=$(pwd);
|
6
|
+
else
|
7
|
+
export PROJECT_DIR=$WORKSPACE/glowworm;
|
8
|
+
fi
|
9
|
+
|
10
|
+
source $PROJECT_DIR/ci_jobs/setup.sh
|
11
|
+
|
12
|
+
echo START TASK: tests
|
13
|
+
exec_rake_task test:units
|
data/ci_jobs/setup.sh
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
#!/usr/bin/env bash -l
|
2
|
+
set -e
|
3
|
+
|
4
|
+
echo START TASK: Determining test config
|
5
|
+
|
6
|
+
export TAP_OUTPUT=true;
|
7
|
+
|
8
|
+
if [[ -z $WORKSPACE ]] ; then
|
9
|
+
export RESULTS_DIR="$(pwd)/results"
|
10
|
+
else
|
11
|
+
export RESULTS_DIR="$WORKSPACE/results"
|
12
|
+
fi
|
13
|
+
|
14
|
+
rm -rf "$RESULTS_DIR"
|
15
|
+
mkdir -p "$RESULTS_DIR"
|
16
|
+
|
17
|
+
cd "$PROJECT_DIR"
|
18
|
+
|
19
|
+
# The ruby versions to test are without the gemset specifier.
|
20
|
+
# Default to testing 1.9.3-p194. Which is the approved 1.9.3 ruby version.
|
21
|
+
: ${TEST_RUBY_VERSION:=1.9.3-p194}
|
22
|
+
|
23
|
+
# Default the server and client ruby versions to TEST_RUBY_VERSION
|
24
|
+
: ${TEST_SERVER_RUBY_VERSION:=$TEST_RUBY_VERSION}
|
25
|
+
: ${TEST_CLIENT_RUBY_VERSION:=$TEST_RUBY_VERSION}
|
26
|
+
|
27
|
+
export TEST_RUBY_VERSION TEST_SERVER_RUBY_VERSION TEST_CLIENT_RUBY_VERSION
|
28
|
+
|
29
|
+
# default to verifying the glowworm code contained in the repo
|
30
|
+
# Other valid values are the glowworm gem version that should be used.
|
31
|
+
: ${TEST_GLOWWORM_VERSION:=latest}
|
32
|
+
|
33
|
+
export TEST_GLOWWORM_VERSION
|
34
|
+
|
35
|
+
# The ruby distribution to test is with gemset specifier
|
36
|
+
export TEST_CLIENT_RUBY_DIST=${TEST_CLIENT_RUBY_VERSION}
|
37
|
+
export TEST_SERVER_RUBY_DIST=${TEST_SERVER_RUBY_VERSION}
|
38
|
+
|
39
|
+
echo END TASK
|
40
|
+
|
41
|
+
echo START TASK: Load test harness ruby dist
|
42
|
+
|
43
|
+
if [[ -z ${TEST_HARNESS_RUBY_VERSION} ]]; then
|
44
|
+
export TEST_HARNESS_RUBY_DIST=1.9.3-p125
|
45
|
+
else
|
46
|
+
export TEST_HARNESS_RUBY_DIST=${TEST_HARNESS_RUBY_VERSION}
|
47
|
+
fi
|
48
|
+
|
49
|
+
echo END TASK
|
50
|
+
|
51
|
+
echo START TASK: init rbenv
|
52
|
+
|
53
|
+
eval "$(rbenv init -)"
|
54
|
+
|
55
|
+
echo END TASK
|
56
|
+
|
57
|
+
function setup_ruby_dist
|
58
|
+
{
|
59
|
+
rbenv shell $1
|
60
|
+
|
61
|
+
gem install --no-rdoc --no-ri bundler
|
62
|
+
|
63
|
+
bundle install
|
64
|
+
}
|
65
|
+
|
66
|
+
echo START TASK: Init ruby distribution ${TEST_SERVER_RUBY_DIST}
|
67
|
+
|
68
|
+
# Setup the server ruby distribution
|
69
|
+
( cd server ; setup_ruby_dist ${TEST_SERVER_RUBY_DIST} );
|
70
|
+
|
71
|
+
echo END TASK
|
72
|
+
|
73
|
+
echo START TASK: Init ruby distribution ${TEST_CLIENT_RUBY_DIST}
|
74
|
+
|
75
|
+
# If the glowworm version is 'latest' then just setup the client using bundle install
|
76
|
+
if [[ $TEST_GLOWWORM_VERSION == latest ]]; then
|
77
|
+
echo using latest;
|
78
|
+
|
79
|
+
setup_ruby_dist ${TEST_CLIENT_RUBY_DIST}
|
80
|
+
else
|
81
|
+
echo using ${TEST_GLOWWORM_VERSION}
|
82
|
+
|
83
|
+
rbenv shell ${TEST_CLIENT_RUBY_DIST}
|
84
|
+
|
85
|
+
gem install --no-rdoc --no-ri bundler
|
86
|
+
|
87
|
+
bundle install --gemfile ${PROJECT_DIR}/test/integration/gemfile_for_specific_glowworm_version
|
88
|
+
fi
|
89
|
+
|
90
|
+
echo Running test harness under ${TEST_HARNESS_RUBY_DIST};
|
91
|
+
|
92
|
+
# setup the test harness ruby distribution as a superset of the client and server distributions
|
93
|
+
# along with additional test harness specific packages.
|
94
|
+
setup_ruby_dist ${TEST_HARNESS_RUBY_DIST};
|
95
|
+
(cd server ; setup_ruby_dist ${TEST_HARNESS_RUBY_DIST});
|
96
|
+
|
97
|
+
# TODO(corey): install via a Gemfile
|
98
|
+
gem install --no-rdoc --no-ri tapout
|
99
|
+
|
100
|
+
echo END TASK
|
101
|
+
|
102
|
+
function translate_TAP_results()
|
103
|
+
{
|
104
|
+
actual_exit_code=$?
|
105
|
+
find "$RESULTS_DIR" -name '*.yaml' -exec bash -c 'tapout tap < {} > {}.tap' \;
|
106
|
+
return $actual_exit_code
|
107
|
+
}
|
108
|
+
|
109
|
+
function exec_rake_task()
|
110
|
+
{
|
111
|
+
# TODO(corey): Have the rake task output each unit test to an individual YAMLish TAP file
|
112
|
+
# Always try to translate the YAMLish TAP to PERLish TAP for jenkins
|
113
|
+
trap translate_TAP_results EXIT
|
114
|
+
|
115
|
+
# Also output to the current STDOUT so the console log has all data.
|
116
|
+
# this is last to assure the exit code of the script is the result of rake
|
117
|
+
rake "$1" "$@" | tee $RESULTS_DIR/results.yaml
|
118
|
+
exit ${PIPESTATUS[0]}
|
119
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "sinatra"
|
5
|
+
require "glowworm"
|
6
|
+
|
7
|
+
Ecology.read("example_server.ecology")
|
8
|
+
|
9
|
+
configure do
|
10
|
+
set :port, "8087"
|
11
|
+
end
|
12
|
+
|
13
|
+
get "/" do
|
14
|
+
if Glowworm.feature_flag(35, "new_signup_message", :ttl => 15)
|
15
|
+
button_text = "join now for free?"
|
16
|
+
else
|
17
|
+
button_text = "sign up?"
|
18
|
+
end
|
19
|
+
|
20
|
+
<<STATIC_PAGE
|
21
|
+
<html>
|
22
|
+
<head>
|
23
|
+
<title>Feature Test App</title>
|
24
|
+
<meta http-equiv="refresh" content="5">
|
25
|
+
</head>
|
26
|
+
<body>
|
27
|
+
<h1> Feature Test App </h1>
|
28
|
+
Why don't you <button>#{button_text}</button>
|
29
|
+
</body>
|
30
|
+
</html>
|
31
|
+
STATIC_PAGE
|
32
|
+
end
|
data/glowworm.gemspec
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require "glowworm/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "glowworm"
|
8
|
+
s.version = Glowworm::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Noah Gibbs"]
|
11
|
+
s.email = ["noah@ooyala.com"]
|
12
|
+
s.homepage = "http://www.ooyala.com"
|
13
|
+
s.summary = %q{Ruby feature management for dark deploys}
|
14
|
+
s.description = <<EOS
|
15
|
+
Glowworm queries and caches feature flags from a central
|
16
|
+
REST server, while balancing quick changes and good performance.
|
17
|
+
EOS
|
18
|
+
|
19
|
+
s.rubyforge_project = "glowworm"
|
20
|
+
|
21
|
+
ignores = File.readlines(".gitignore").grep(/\S+/).map {|pattern| pattern.chomp }
|
22
|
+
ignores += ["server/config/*", "server/Capfile", "server/shared_lib/*",
|
23
|
+
"server/etc/*", "server/tools/*"]
|
24
|
+
dotfiles = Dir[".*"]
|
25
|
+
s.files = Dir["**/*"].reject {|f| File.directory?(f) || ignores.any? {|i| File.fnmatch(i, f) } } + dotfiles
|
26
|
+
s.test_files = s.files.grep(/^test\//)
|
27
|
+
s.executables << "glowworm"
|
28
|
+
|
29
|
+
s.bindir = 'bin'
|
30
|
+
s.require_paths = ["lib"]
|
31
|
+
|
32
|
+
s.add_dependency "ecology", "~>0.0.18"
|
33
|
+
s.add_dependency "termite", "~>0.0.13"
|
34
|
+
s.add_dependency "httparty"
|
35
|
+
s.add_dependency "multi_json"
|
36
|
+
s.add_dependency "trollop"
|
37
|
+
|
38
|
+
s.add_development_dependency "bundler"
|
39
|
+
s.add_development_dependency "scope", "~>0.2.1"
|
40
|
+
s.add_development_dependency "nodule", "~>0.0.25"
|
41
|
+
s.add_development_dependency "mocha"
|
42
|
+
s.add_development_dependency "sqlite3"
|
43
|
+
s.add_development_dependency "rake"
|
44
|
+
s.add_development_dependency "minitap", ">= 0.3.5"
|
45
|
+
s.add_development_dependency "sinatra"
|
46
|
+
s.add_development_dependency "sequel"
|
47
|
+
s.add_development_dependency "mysql"
|
48
|
+
s.add_development_dependency "rack-ooyala-headers"
|
49
|
+
s.add_development_dependency "yajl-ruby"
|
50
|
+
s.add_development_dependency "tapout"
|
51
|
+
s.add_development_dependency "sinatra-synchrony"
|
52
|
+
s.add_development_dependency "thin"
|
53
|
+
s.add_development_dependency "em-net-http"
|
54
|
+
end
|
data/lib/glowworm.rb
ADDED
@@ -0,0 +1,501 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "glowworm/version"
|
3
|
+
|
4
|
+
require "ecology"
|
5
|
+
require "termite"
|
6
|
+
require "httparty"
|
7
|
+
require "uri"
|
8
|
+
|
9
|
+
# TODO(noah): convert log warnings to Hastur calls when they're actually stats.
|
10
|
+
# Right now our console output is *way* too chatty, but we don't want to lose
|
11
|
+
# this information when syslog picks what to save.
|
12
|
+
|
13
|
+
module Glowworm
|
14
|
+
class << self
|
15
|
+
attr_accessor :termite_logger
|
16
|
+
attr_accessor :thread_mutex
|
17
|
+
attr_accessor :cache_mutex
|
18
|
+
attr_accessor :ttl
|
19
|
+
attr_accessor :timeout
|
20
|
+
attr_reader :server
|
21
|
+
attr_reader :threadless
|
22
|
+
|
23
|
+
# Obsoleted on Feb 15, 2012
|
24
|
+
attr_reader :warm_on_boot
|
25
|
+
attr_reader :silent_failure
|
26
|
+
|
27
|
+
Glowworm.thread_mutex = Mutex.new
|
28
|
+
Glowworm.cache_mutex = Mutex.new
|
29
|
+
|
30
|
+
def warm_on_boot=(value)
|
31
|
+
@warm_on_boot = value
|
32
|
+
puts "Glowworm.warm_on_boot is obsolete as of Feb 15, 2012."
|
33
|
+
puts "You can just remove the line that assigns it."
|
34
|
+
Glowworm.termite_logger.warn("Used obsolete GW.warm_on_boot") if Glowworm.termite_logger
|
35
|
+
end
|
36
|
+
|
37
|
+
def silent_failure=(value)
|
38
|
+
@silent_failure = value
|
39
|
+
puts "Glowworm.silent_failure is obsolete as of Feb 15, 2012."
|
40
|
+
puts "You can just remove the line that assigns it."
|
41
|
+
Glowworm.termite_logger.warn("Used obsolete GW.silent_failure") if Glowworm.termite_logger
|
42
|
+
end
|
43
|
+
|
44
|
+
def server=(value)
|
45
|
+
@server = value
|
46
|
+
return if value.nil?
|
47
|
+
unless value[0..4] == "http:"
|
48
|
+
@server = "http://#{value}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get the value of a single feature for a single account
|
53
|
+
# This is equivalent to calling feature_flag with option
|
54
|
+
# get_value => true
|
55
|
+
#
|
56
|
+
# Options: (see feature_flag)
|
57
|
+
def feature_value(account, feature, options = {})
|
58
|
+
feature_flag(account, feature, options.merge(:get_value => true))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return the freshness in seconds of this account/feature
|
62
|
+
# combination. If nil is returned, we don't have actual server
|
63
|
+
# data for this combination.
|
64
|
+
#
|
65
|
+
# Note that just calling freshness won't check for updates. You
|
66
|
+
# need to call prefetch or feature_flag if you want updates.
|
67
|
+
#
|
68
|
+
def freshness(account, feature, options = {})
|
69
|
+
@last_success
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get whether a single feature is enabled for a single account
|
73
|
+
#
|
74
|
+
# Options:
|
75
|
+
# ttl: how long server data stays fresh before re-fetching
|
76
|
+
# timeout: how long to wait for a response before returning stale data
|
77
|
+
# get_value: true to return actual value rather than true/false
|
78
|
+
# defaults to false
|
79
|
+
def feature_flag(account, feature, options = {})
|
80
|
+
Ecology.read # Ecology must be initialized by now
|
81
|
+
|
82
|
+
if account.nil? || !account.respond_to?(:to_s)
|
83
|
+
raise "You must provide a string-castable account, not #{account.inspect}!"
|
84
|
+
end
|
85
|
+
if feature.nil? || !feature.respond_to?(:to_s)
|
86
|
+
raise "You must provide a string-castable feature, not #{feature.inspect}!"
|
87
|
+
end
|
88
|
+
|
89
|
+
account = account.to_s
|
90
|
+
feature = feature.to_s
|
91
|
+
|
92
|
+
status = fetch_from_server(account, feature, options)
|
93
|
+
if status.nil?
|
94
|
+
status = 504
|
95
|
+
Glowworm.termite_logger.debug "Server didn't respond within #{options[:timeout] || Glowworm.timeout} seconds"
|
96
|
+
else
|
97
|
+
Glowworm.termite_logger.debug "Received status #{status}"
|
98
|
+
end
|
99
|
+
|
100
|
+
local_account_set_cache = local_feature_cache = local_override_cache = local_fully_active_cache = nil
|
101
|
+
|
102
|
+
Glowworm.cache_mutex.synchronize do
|
103
|
+
# Store references to caches locally within mutex, then use these
|
104
|
+
local_account_set_cache = @account_set_cache
|
105
|
+
local_feature_cache = @feature_cache
|
106
|
+
local_override_cache = @override_cache
|
107
|
+
local_fully_active_cache = @fully_active_cache
|
108
|
+
end
|
109
|
+
|
110
|
+
unless local_override_cache && local_account_set_cache && local_feature_cache
|
111
|
+
Glowworm.termite_logger.debug "Caches aren't instantiated at all..."
|
112
|
+
return options[:default] if options.has_key?(:default)
|
113
|
+
Glowworm.termite_logger.error("You don't seem to have ever received any data from #{Glowworm.server}. " +
|
114
|
+
"Is Glowworm misconfigured?")
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
118
|
+
get_value = options[:get_value] || false
|
119
|
+
|
120
|
+
if local_override_cache[feature] && local_override_cache[feature].has_key?(account)
|
121
|
+
return get_value ? local_override_cache[feature][account][:value] :
|
122
|
+
local_override_cache[feature][account][:flag]
|
123
|
+
end
|
124
|
+
|
125
|
+
account_set = local_account_set_cache[account]
|
126
|
+
if account_set && local_feature_cache[feature]
|
127
|
+
value = nil
|
128
|
+
account_set.sort.each do |single_set|
|
129
|
+
if get_value
|
130
|
+
value ||= local_feature_cache[feature][single_set]
|
131
|
+
else
|
132
|
+
value ||= local_feature_cache[feature].has_key?(single_set)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
else
|
136
|
+
# Server has nothing for this account and feature.
|
137
|
+
Glowworm.termite_logger.debug "Can't find #{feature} for #{account}"
|
138
|
+
end
|
139
|
+
|
140
|
+
return value unless value.nil?
|
141
|
+
|
142
|
+
# At this point, there is no information about this feature for this specific account.
|
143
|
+
# We fall back to default first, and then true or false depending on if the feature is fully active.
|
144
|
+
if local_fully_active_cache && local_fully_active_cache[feature]
|
145
|
+
# Currently, fully_active isn't available for get_value
|
146
|
+
# features.
|
147
|
+
fully_active = local_fully_active_cache[feature]
|
148
|
+
end
|
149
|
+
|
150
|
+
message = "This feature has never been cached for the given account! "
|
151
|
+
unless (local_feature_cache || {}).has_key?(feature)
|
152
|
+
message += "Feature '#{feature[0...1_000]}' seems not to exist. "
|
153
|
+
end
|
154
|
+
unless (local_account_set_cache || {}).has_key?(account)
|
155
|
+
message += "Account '#{account[0...1_000]}' seems not to exist. "
|
156
|
+
end
|
157
|
+
(account_set || []).each do |single_set|
|
158
|
+
if local_feature_cache[feature].nil? || !local_feature_cache[feature].has_key?(single_set)
|
159
|
+
unless single_set.nil?
|
160
|
+
message += "There is no data for account set " + single_set.to_s[0...1_000] + " for this feature."
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
Glowworm.termite_logger.warn message
|
166
|
+
|
167
|
+
return options[:default] if options.has_key?(:default)
|
168
|
+
|
169
|
+
unless fully_active.nil?
|
170
|
+
# Currently, fully_active isn't available for get_value
|
171
|
+
# features.
|
172
|
+
Glowworm.termite_logger.info("This feature is fully active. Returning true.")
|
173
|
+
fully_active
|
174
|
+
else
|
175
|
+
Glowworm.termite_logger.warn("No default, feature or account never cached: #{message}")
|
176
|
+
false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Prefetch feature data from the server if stale.
|
181
|
+
#
|
182
|
+
# account - an account or :all
|
183
|
+
# feature - a feature or :all
|
184
|
+
#
|
185
|
+
# Options:
|
186
|
+
# ttl: how long server data stays fresh
|
187
|
+
# timeout: how long to wait on the server response
|
188
|
+
# raise: raise an exception if the prefetch fails or times out
|
189
|
+
def prefetch(account, feature, options = {})
|
190
|
+
Ecology.read # Ecology must be initialized by now
|
191
|
+
|
192
|
+
account != nil && account.respond_to?(:to_s) or
|
193
|
+
raise "You must provide a string-castable account, not #{account.inspect}!"
|
194
|
+
feature != nil && feature.respond_to?(:to_s) or
|
195
|
+
raise "You must provide a string-castable feature, not #{feature.inspect}!"
|
196
|
+
|
197
|
+
account = account.to_s
|
198
|
+
feature = feature.to_s
|
199
|
+
|
200
|
+
# By default a prefetch shouldn't wait at all
|
201
|
+
options[:timeout] ||= 0.0
|
202
|
+
|
203
|
+
Glowworm.termite_logger.warn "Glowworm prefetch: '#{account}', '#{feature}'"
|
204
|
+
fetch_from_server(account, feature, options)
|
205
|
+
end
|
206
|
+
|
207
|
+
def reset
|
208
|
+
@account_set_cache = nil
|
209
|
+
@feature_cache = nil
|
210
|
+
@override_cache = nil
|
211
|
+
@fully_active_cache = nil
|
212
|
+
@last_md5 = nil
|
213
|
+
@last_fetch_time = nil
|
214
|
+
@last_success = nil
|
215
|
+
@background_thread = nil
|
216
|
+
@last_modified = nil
|
217
|
+
@threadless = nil
|
218
|
+
@em = nil
|
219
|
+
|
220
|
+
Glowworm.ttl = nil
|
221
|
+
Glowworm.timeout = nil
|
222
|
+
Glowworm.server = nil
|
223
|
+
Glowworm.termite_logger = nil
|
224
|
+
end
|
225
|
+
|
226
|
+
def all_cached_accounts
|
227
|
+
# This accessor already duplicates, so we
|
228
|
+
# can just return it.
|
229
|
+
@account_set_cache ? @account_set_cache.keys : []
|
230
|
+
end
|
231
|
+
|
232
|
+
def all_cached_features
|
233
|
+
# This accessor already duplicates, so we
|
234
|
+
# can just return it.
|
235
|
+
@feature_cache ? @feature_cache.keys : []
|
236
|
+
end
|
237
|
+
|
238
|
+
def account_set_cache
|
239
|
+
# Duplicate the cache so that the caller can't
|
240
|
+
# change cache entries - that would be bad.
|
241
|
+
@account_set_cache ? @account_set_cache.dup : nil
|
242
|
+
end
|
243
|
+
|
244
|
+
def feature_cache
|
245
|
+
# Duplicate the cache so that the caller can't
|
246
|
+
# change cache entries - that would be bad.
|
247
|
+
@feature_cache ? @feature_cache.dup : nil
|
248
|
+
end
|
249
|
+
|
250
|
+
def override_cache
|
251
|
+
# Duplicate the cache so that the caller can't
|
252
|
+
# change cache entries - that would be bad.
|
253
|
+
@override_cache ? @override_cache.dup : nil
|
254
|
+
end
|
255
|
+
|
256
|
+
def fully_active_cache
|
257
|
+
# Duplicate the cache so that the caller can't
|
258
|
+
# change cache entries - that would be bad.
|
259
|
+
@fully_active_cache ? @fully_active_cache.dup : nil
|
260
|
+
end
|
261
|
+
|
262
|
+
def last_md5
|
263
|
+
# Duplicate the md5 so that the caller can't
|
264
|
+
# change it - that would be bad.
|
265
|
+
@last_md5 ? @last_md5.dup : nil
|
266
|
+
end
|
267
|
+
|
268
|
+
# Sychronously update cache. Raises an error if the update fails.
|
269
|
+
# This will wrap in a EM synchrony block if eventmachine is being used, or update regularly otherwise.
|
270
|
+
def update_cache_in_foreground
|
271
|
+
if @em
|
272
|
+
begin
|
273
|
+
EM.synchrony do
|
274
|
+
_update_cache_in_foreground_
|
275
|
+
end
|
276
|
+
rescue NameError
|
277
|
+
_update_cache_in_foreground_
|
278
|
+
rescue NoMethodError
|
279
|
+
raise 'Eventmachine is being used without requiring "glowworm/em". Please use this require.'
|
280
|
+
end
|
281
|
+
else
|
282
|
+
_update_cache_in_foreground_
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def require_em
|
287
|
+
begin
|
288
|
+
require "em-synchrony"
|
289
|
+
require "em-net-http"
|
290
|
+
rescue LoadError
|
291
|
+
raise "You must have em/sinatra-synchrony AND em-net-http installed for the Glowworm.em to work"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# If you are using EM, but can't call "glowworm/em" at require time, then set Glowworm.em
|
296
|
+
# from within your fiber.
|
297
|
+
def em
|
298
|
+
Ecology.read
|
299
|
+
Glowworm.timeout = 0
|
300
|
+
@em = true
|
301
|
+
@threadless = true
|
302
|
+
require_em
|
303
|
+
update_cache_in_foreground
|
304
|
+
end
|
305
|
+
|
306
|
+
def no_bg
|
307
|
+
Ecology.read
|
308
|
+
Glowworm.timeout = 0
|
309
|
+
@threadless = true
|
310
|
+
# This first call should be the EM-free version, as at require time EM.synchrony causes a hang.
|
311
|
+
_update_cache_in_foreground_
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
# This functions is marked private, as it should only be called at require time.
|
317
|
+
# This is because the internal _update_cache_in_foreground_ is called, which means that we expect
|
318
|
+
# to be in a require-time architecture, and not yet with EM loaded.
|
319
|
+
def _em_
|
320
|
+
no_bg
|
321
|
+
@em = true
|
322
|
+
end
|
323
|
+
|
324
|
+
private
|
325
|
+
|
326
|
+
def _update_cache_in_foreground_
|
327
|
+
account = feature = :all
|
328
|
+
url = url_for_account_and_feature(account, feature)
|
329
|
+
update_cache(account, feature, url, :foreground => true)
|
330
|
+
end
|
331
|
+
|
332
|
+
def url_for_account_and_feature(account, feature)
|
333
|
+
url = Glowworm.server + "/all_if_modified"
|
334
|
+
url += "?md5=#{@last_md5}" if @last_md5
|
335
|
+
url
|
336
|
+
end
|
337
|
+
|
338
|
+
def fetch_from_server(account, feature, options = {})
|
339
|
+
ttl = options[:ttl] || Glowworm.ttl
|
340
|
+
timeout = options[:timeout] || Glowworm.timeout
|
341
|
+
|
342
|
+
# If no background thread, rely on prefetched data
|
343
|
+
return 204 if @threadless
|
344
|
+
|
345
|
+
# Return cached value if it's within TTL
|
346
|
+
time_now = Time.now # Call Time.now once for consistency
|
347
|
+
|
348
|
+
if @last_fetch_time
|
349
|
+
freshness = time_now - @last_fetch_time
|
350
|
+
|
351
|
+
Glowworm.termite_logger.debug "Freshness: #{freshness} seconds"
|
352
|
+
if freshness < ttl
|
353
|
+
Glowworm.termite_logger.warn "Already fresh: account #{account}, feature: #{feature}," +
|
354
|
+
"TTL is #{ttl}"
|
355
|
+
return 200
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
Glowworm.cache_mutex.synchronize do
|
360
|
+
@account_set_cache ||= {}
|
361
|
+
@feature_cache ||= {}
|
362
|
+
@override_cache ||= {}
|
363
|
+
@fully_active_cache ||= {}
|
364
|
+
end
|
365
|
+
duration = Time.now - time_now
|
366
|
+
|
367
|
+
bg_thread = nil
|
368
|
+
|
369
|
+
Glowworm.thread_mutex.synchronize do
|
370
|
+
bg_thread = @background_thread
|
371
|
+
|
372
|
+
if bg_thread
|
373
|
+
Glowworm.termite_logger.debug "Using existing background thread"
|
374
|
+
else
|
375
|
+
Glowworm.termite_logger.debug "Creating new background thread"
|
376
|
+
bg_thread = @background_thread = Thread.new do
|
377
|
+
situation = "querying server #{Glowworm.server.inspect}, account #{account}, feature #{feature}"
|
378
|
+
|
379
|
+
begin
|
380
|
+
url = url_for_account_and_feature(account, feature)
|
381
|
+
update_cache(account, feature, url)
|
382
|
+
Glowworm.termite_logger.warn "Succeeded in getting updated Glowworm feature: '#{account}', '#{feature}'"
|
383
|
+
rescue Exception # catch everything
|
384
|
+
Glowworm.termite_logger.warn "Exception #{situation}, message '#{$!.message}'"
|
385
|
+
Glowworm.termite_logger.info "Exception #{situation}, message '#{$!.message}'," +
|
386
|
+
"backtrace '#{$!.backtrace.inspect}'"
|
387
|
+
|
388
|
+
raise if options[:raise]
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
if bg_thread.join(timeout)
|
395
|
+
Glowworm.termite_logger.debug "Successfully joined background thread"
|
396
|
+
|
397
|
+
status = bg_thread[:last_status]
|
398
|
+
|
399
|
+
Glowworm.thread_mutex.synchronize { @background_thread = nil if @background_thread == bg_thread }
|
400
|
+
|
401
|
+
status
|
402
|
+
else
|
403
|
+
Glowworm.termite_logger.debug "Join returned false after #{timeout} seconds, returning 504"
|
404
|
+
504
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def update_cache(account, feature, url, options = {})
|
409
|
+
situation = "querying server #{url}, all info, checksum #{@last_md5.inspect}"
|
410
|
+
|
411
|
+
start_time = Time.now
|
412
|
+
response = HTTParty.get(url, :headers => { "if-modified-since" => @last_modified || "" })
|
413
|
+
|
414
|
+
latency = Time.now - start_time
|
415
|
+
Glowworm.termite_logger.debug "Request latency: #{latency} seconds"
|
416
|
+
|
417
|
+
@last_modified = response.headers["last-modified"]
|
418
|
+
|
419
|
+
if options[:foreground]
|
420
|
+
raise "Cache update failed with response code #{response.code}!" unless [200, 204, 304].include? response.code
|
421
|
+
else
|
422
|
+
Thread.current[:last_status] = response.code
|
423
|
+
end
|
424
|
+
@last_fetch_time = Time.now
|
425
|
+
if response.code >= 200 && response.code < 300
|
426
|
+
Glowworm.termite_logger.info "Success #{situation}, response code #{response.code}, response size #{response.body.size}",
|
427
|
+
{}, :component => "glowworm"
|
428
|
+
refresh_cache_from_response(response.body)
|
429
|
+
@last_success = @last_fetch_time
|
430
|
+
elsif response.code >= 300 && response.code < 400
|
431
|
+
# Normally this is a 304 unmodified - no update
|
432
|
+
@last_success = @last_fetch_time
|
433
|
+
Glowworm.termite_logger.info "Up to date when #{situation}, response code #{response.code}"
|
434
|
+
elsif response.code >= 400 && response.code < 500
|
435
|
+
# This is a 404 or similar - not found.
|
436
|
+
Glowworm.termite_logger.warn("Failure #{situation}, response code #{response.code}, not found",
|
437
|
+
{}, :component => "glowworm")
|
438
|
+
else
|
439
|
+
Glowworm.termite_logger.warn("Failure #{situation}, response code #{response.code}",
|
440
|
+
{}, :component => "glowworm")
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def refresh_cache_from_response(response)
|
445
|
+
# In order to keep the amount of mutex-locked code to a minimum, we will pre-construct the hashes locally,
|
446
|
+
# and then populate the caches with these local variables within a mutex
|
447
|
+
data = MultiJson.load(response)
|
448
|
+
|
449
|
+
raise "Wrong Glowworm protocol version!" unless data["version"] == 1
|
450
|
+
|
451
|
+
local_account_set_cache = {}
|
452
|
+
(data["account_sets"] || {}).each do |account, account_set|
|
453
|
+
local_account_set_cache[account] = account_set.map(&:to_i)
|
454
|
+
end
|
455
|
+
|
456
|
+
local_feature_cache = {}
|
457
|
+
(data["features"] || {}).each do |feature, account_sets|
|
458
|
+
local_feature_cache[feature] = {}
|
459
|
+
account_sets.each do |account_set, value|
|
460
|
+
# Store the value of this account_set for this feature
|
461
|
+
local_feature_cache[feature][account_set.to_i] = value
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
local_override_cache = {}
|
466
|
+
(data["overrides"] || {}).each do |feature, overrides|
|
467
|
+
local_override_cache[feature] = {}
|
468
|
+
overrides.each do |account, value|
|
469
|
+
local_override_cache[feature][account] = { :flag => value != 0, :value => value }
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
local_fully_active_cache = {}
|
474
|
+
(data["fully_active"] || {}).each do |feature, fully_active|
|
475
|
+
local_fully_active_cache[feature] = fully_active
|
476
|
+
end
|
477
|
+
|
478
|
+
local_last_md5 = data["md5"]
|
479
|
+
Glowworm.cache_mutex.synchronize do
|
480
|
+
@account_set_cache = local_account_set_cache
|
481
|
+
@feature_cache = local_feature_cache
|
482
|
+
@override_cache = local_override_cache
|
483
|
+
@fully_active_cache = local_fully_active_cache
|
484
|
+
@last_md5 = local_last_md5
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
Ecology.on_initialize do
|
489
|
+
Glowworm.termite_logger ||= Termite::Logger.new(:component => "glowworm", :use_logger_prefix => true)
|
490
|
+
Glowworm.ttl ||= Ecology.property("features:ttl") || Ecology.property("features:refresh") || 300
|
491
|
+
Glowworm.timeout ||= Ecology.property("features:timeout") || 0.1
|
492
|
+
Glowworm.server ||= ENV["GW_URI"] || (Ecology.property("features:server") || "http://localhost:4999")
|
493
|
+
end
|
494
|
+
|
495
|
+
# When Ecology.reset is called, reset Glowworm too.
|
496
|
+
# Usually this is for testing.
|
497
|
+
Ecology.on_reset do
|
498
|
+
Glowworm.reset
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|