glowworm 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +6 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +129 -0
  6. data/LICENSE +19 -0
  7. data/README.md +326 -0
  8. data/Rakefile +29 -0
  9. data/bin/basic_server_tester +311 -0
  10. data/bin/em_server/config.ru +2 -0
  11. data/bin/em_server/em_server.rb +19 -0
  12. data/bin/em_server_tester +68 -0
  13. data/bin/glowworm +90 -0
  14. data/bin/load_tester +84 -0
  15. data/ci_jobs/glowworm-continuous-deploy-next-staging/run.sh +10 -0
  16. data/ci_jobs/glowworm-integrations/run.sh +15 -0
  17. data/ci_jobs/glowworm-performance/run.sh +2 -0
  18. data/ci_jobs/glowworm-robustness/run.sh +2 -0
  19. data/ci_jobs/glowworm-units/run.sh +13 -0
  20. data/ci_jobs/setup.sh +119 -0
  21. data/example/example_server.ecology +6 -0
  22. data/example/example_server.rb +32 -0
  23. data/glowworm.gemspec +54 -0
  24. data/lib/glowworm.rb +501 -0
  25. data/lib/glowworm/em.rb +8 -0
  26. data/lib/glowworm/no_bg.rb +2 -0
  27. data/lib/glowworm/version.rb +3 -0
  28. data/server/Gemfile +27 -0
  29. data/server/Gemfile.lock +87 -0
  30. data/server/PROTOCOL +39 -0
  31. data/server/check_mk_checks/check_glowworm_server +43 -0
  32. data/server/db_migrations/20111004214649_change_feature_accounts_to_string.rb +60 -0
  33. data/server/db_migrations/20111028104546_add_value_to_account_set_features.rb +12 -0
  34. data/server/db_migrations/20120217090636_add_fully_active_flag_to_features.rb +15 -0
  35. data/server/example_test_data.rb +66 -0
  36. data/server/glowworm_server.ecology.erb +16 -0
  37. data/server/glowworm_server.rb +226 -0
  38. data/server/run/server.sh +7 -0
  39. data/server/server_test.rb +72 -0
  40. data/server/version.rb +3 -0
  41. data/test/integration/basic_server_test.rb +90 -0
  42. data/test/integration/create_sqlite_data.rb +196 -0
  43. data/test/integration/em_server_test.rb +68 -0
  44. data/test/integration/gemfile_for_specific_glowworm_version +17 -0
  45. data/test/integration/gemfile_for_specific_glowworm_version.lock +55 -0
  46. data/test/integration/integration_test_helper.rb +153 -0
  47. data/test/integration/load_test.rb +59 -0
  48. data/test/integration/nginx.conf +23 -0
  49. data/test/integration/server_test.ecology.erb +6 -0
  50. data/test/test_helper.rb +47 -0
  51. data/test/units/em_test.rb +41 -0
  52. data/test/units/feature_flag_test.rb +297 -0
  53. data/test/units/no_bg_test.rb +40 -0
  54. data/test/units/request_test.rb +51 -0
  55. metadata +410 -0
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,2 @@
1
+ #!/usr/bin/env bash -l
2
+ exit 1
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash -l
2
+ exit 1
@@ -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,6 @@
1
+ {
2
+ "application": "example of using Glowworm",
3
+ "features": {
4
+ "server": "http://localhost:4999"
5
+ }
6
+ }
@@ -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