-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from Talkdesk/bootstrap-gem
Bootstrap first gem version
- Loading branch information
Showing
17 changed files
with
767 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/.bundle/ | ||
/.yardoc | ||
/Gemfile.lock | ||
/_yardoc/ | ||
/coverage/ | ||
/doc/ | ||
/pkg/ | ||
/spec/reports/ | ||
/tmp/ | ||
|
||
# rspec failure tracking | ||
.rspec_status | ||
spec/examples.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--require spec_helper | ||
--color |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
jruby-9.1.12.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
source 'https://rubygems.org' | ||
|
||
# Specify your gem's dependencies in warm-blanket.gemspec | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# WarmBlanket | ||
|
||
**WarmBlanket is still a prototype. YMMV** | ||
|
||
WarmBlanket is a Ruby gem for warming up web services on boot. Its main target are JRuby web services, although it is not JRuby-specific in any way. | ||
|
||
* [WarmBlanket](#warmblanket) | ||
* [How the magic happens](#how-the-magic-happens) | ||
* [Why do we need to warm up web services?](#why-do-we-need-to-warm-up-web-services) | ||
* [What does WarmBlanket do?](#what-does-warmblanket-do) | ||
* [How does WarmBlanket work?](#how-does-warmblanket-work) | ||
* [Limitations/caveats](#limitationscaveats) | ||
* [How can I make use of it?](#how-can-i-make-use-of-it) | ||
* [Installation](#installation) | ||
* [Configuration settings](#configuration-settings) | ||
* [Configuring endpoints to be called](#configuring-endpoints-to-be-called) | ||
* [Trigger warmup](#trigger-warmup) | ||
|
||
# How the magic happens | ||
|
||
## Why do we need to warm up web services? | ||
|
||
When the Java Virtual Machine (JVM) starts, it starts by interpreting Java bytecode. As it starts to detect code that runs often, it just-in-time compiles that code into native machine code, improving performance. | ||
|
||
This is a known challenge for most JVMs, and the same applies to JRuby applications, which also run on the JVM. | ||
|
||
A widely-documented solution to this problem is to perform a warm-up step when starting a service: | ||
|
||
* <https://landing.google.com/sre/book/chapters/load-balancing-datacenter.html#unpredictable-performance-factors-JMs7i7trCj> | ||
* <http://www.brendangregg.com/blog/2016-09-28/java-warmup.html> | ||
* <https://devcenter.heroku.com/articles/warming-up-a-java-process> | ||
|
||
## What does WarmBlanket do? | ||
|
||
WarmBlanket warms services by performing repeated web requests for a configurable number of seconds. After that time, it closes shop and you'll never hear about it until the next service restart or deploy. | ||
|
||
## How does WarmBlanket work? | ||
|
||
WarmBlanket spawns a configurable number of background threads that run inside the service process, and then uses an http client to perform local requests to the web server, simulating load. | ||
|
||
As it simulates requests, the JVM is warmed up and thus when real requests come in, no performance degradation is observed. | ||
|
||
## Limitations/caveats | ||
|
||
We strongly recommend that any services using WarmBlanket, if deployed on Heroku, use [Preboot](https://devcenter.heroku.com/articles/preboot). Preboot allows a service instance to be warmed up for 3 minutes before Heroku starts sending live traffic its way, which is preferable to doing it live. | ||
|
||
# How can I make use of it? | ||
|
||
To make use of WarmBlanket, you'll need to follow the next sections, which will guide you through installing, configuring and enabling the gem. | ||
|
||
## Installation | ||
|
||
To install using Bundler, add the following to your `Gemfile`: | ||
|
||
```ruby | ||
gem 'warm-blanket', '~> 0.1', | ||
git: 'https://github.com/Talkdesk/warm-blanket.git' | ||
``` | ||
|
||
To install a particular version, add the `tag` option: | ||
|
||
```ruby | ||
gem 'warm-blanket', '~> 0.1', | ||
git: 'https://github.com/Talkdesk/warm-blanket.git', | ||
tag: 'v0.1.0' | ||
``` | ||
|
||
## Configuration settings | ||
|
||
This gem can be configured via the following environment variables: | ||
|
||
* `PORT`: Local webserver port (automatically set on Heroku) | ||
* `WARMBLANKET_ENABLED`: Enable warmup (defaults to `false`; `true` or `1` enables) | ||
* `WARMBLANKET_WARMUP_THREADS`: Number of warmup threads to use (defaults to `2`) | ||
* `WARMBLANKET_WARMUP_TIME_SECONDS`: Time, in seconds, during which to warm up the service (defaults to `150`) | ||
|
||
### Configuring endpoints to be called | ||
|
||
Configure endpoints to be called as follows (on a `config/warm_blanket.rb`: | ||
|
||
```ruby | ||
require 'warm-blanket' | ||
|
||
WarmBlanket.configure do |config| | ||
common_headers = { | ||
'X-Api-Key': ENV['API_KEY'].split(',').first, | ||
} | ||
|
||
config.endpoints = [ | ||
{get: '/apps', headers: common_headers}, | ||
{get: '/', headers: common_headers}, | ||
] | ||
end | ||
``` | ||
|
||
Other HTTP verbs are supported (and you can pass in a `body` key if needed), but be careful about side effects from such verbs. And if there's no side effect from a `POST` or `PUT`, do consider if it shouldn't be a `GET` instead ;) | ||
|
||
## Trigger warmup | ||
|
||
Add the following to the end of your `config.ru` file: | ||
|
||
```ruby | ||
WarmBlanket.trigger_warmup | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
# | ||
# This file was generated by Bundler. | ||
# | ||
# The application 'pry' is installed as part of a gem, and | ||
# this file is here to facilitate running it. | ||
# | ||
|
||
require 'pathname' | ||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', | ||
Pathname.new(__FILE__).realpath) | ||
|
||
require 'rubygems' | ||
require 'bundler/setup' | ||
|
||
load Gem.bin_path('pry', 'pry') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
# | ||
# This file was generated by Bundler. | ||
# | ||
# The application 'rspec' is installed as part of a gem, and | ||
# this file is here to facilitate running it. | ||
# | ||
|
||
require 'pathname' | ||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', | ||
Pathname.new(__FILE__).realpath) | ||
|
||
require 'rubygems' | ||
require 'bundler/setup' | ||
|
||
load Gem.bin_path('rspec-core', 'rspec') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
require 'warm_blanket/version' | ||
require 'warm_blanket/orchestrator' | ||
|
||
require 'dry-configurable' | ||
require 'logging' | ||
|
||
module WarmBlanket | ||
extend Dry::Configurable | ||
|
||
# Endpoints to be called for warmup, see README | ||
setting :endpoints, [], reader: true | ||
|
||
setting :logger, Logging.logger[self], reader: true | ||
|
||
# Local webserver port | ||
setting :port, ENV['PORT'], reader: true | ||
|
||
# Enable warmup | ||
setting :enabled, ENV['WARMBLANKET_ENABLED'], reader: true | ||
|
||
# Number of threads to use | ||
setting :warmup_threads, Integer(ENV['WARMBLANKET_WARMUP_THREADS'] || 2), reader: true | ||
|
||
# Time, in seconds, during which to warm up the service | ||
setting :warmup_time_seconds, Float(ENV['WARMBLANKET_WARMUP_TIME_SECONDS'] || 150), reader: true | ||
|
||
def self.trigger_warmup(logger: WarmBlanket.config.logger, orchestrator_factory: Orchestrator) | ||
unless [true, 'true', '1'].include?(WarmBlanket.config.enabled) | ||
logger.info "WarmBlanket not enabled, ignoring trigger_warmup" | ||
return false | ||
end | ||
|
||
orchestrator_factory.new.call | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'warm_blanket/requester' | ||
require 'warm_blanket/wait_for_port' | ||
|
||
module WarmBlanket | ||
# Orchestrates threads to wait for the port to open and to perform the warmup requests | ||
class Orchestrator | ||
|
||
DEFAULT_HEADERS = { | ||
'X-Forwarded-Proto': 'https', | ||
'X-Request-Id': 'WarmBlanket', | ||
'X-Platform-Tid': 'WarmBlanket', | ||
'X-Client-Id': 'WarmBlanket', | ||
'X-Account': 'WarmBlanket', | ||
}.freeze | ||
|
||
private | ||
|
||
attr_reader :requester_factory | ||
attr_reader :wait_for_port_factory | ||
attr_reader :logger | ||
attr_reader :endpoints | ||
attr_reader :hostname | ||
attr_reader :port | ||
attr_reader :warmup_threads | ||
attr_reader :warmup_time_seconds | ||
|
||
public | ||
|
||
def initialize( | ||
requester_factory: Requester, | ||
wait_for_port_factory: WaitForPort, | ||
logger: WarmBlanket.config.logger, | ||
endpoints: WarmBlanket.config.endpoints, | ||
hostname: 'localhost', | ||
port: WarmBlanket.config.port, | ||
warmup_threads: WarmBlanket.config.warmup_threads, | ||
warmup_time_seconds: WarmBlanket.config.warmup_time_seconds | ||
) | ||
raise "Warmup threads cannot be less than 1 (got #{warmup_threads})" if warmup_threads < 1 | ||
|
||
@requester_factory = requester_factory | ||
@wait_for_port_factory = wait_for_port_factory | ||
@logger = logger | ||
@endpoints = endpoints | ||
@hostname = hostname | ||
@port = port | ||
@warmup_threads = warmup_threads | ||
@warmup_time_seconds = warmup_time_seconds | ||
end | ||
|
||
def call | ||
safely_spawn_thread do | ||
logger.debug 'Started orchestrator thread' | ||
orchestrate | ||
end | ||
end | ||
|
||
private | ||
|
||
def safely_spawn_thread(&block) | ||
Thread.new do | ||
begin | ||
block.call | ||
rescue => e | ||
logger.error "Caught error that caused background thread to die #{e.class}: #{e.message}" | ||
end | ||
end | ||
end | ||
|
||
def orchestrate | ||
success = wait_for_port_to_open | ||
|
||
spawn_warmup_threads if success | ||
end | ||
|
||
def wait_for_port_to_open | ||
wait_for_port_factory.new(port: port).call | ||
end | ||
|
||
def spawn_warmup_threads | ||
# Create remaining threads | ||
(warmup_threads - 1).times do | ||
safely_spawn_thread do | ||
perform_warmup_requests | ||
end | ||
end | ||
|
||
# Reuse current thread | ||
perform_warmup_requests | ||
end | ||
|
||
def perform_warmup_requests | ||
success = false | ||
logger.debug "Starting warmup requests" | ||
|
||
warmup_start = Time.now | ||
warmup_deadline = warmup_start + warmup_time_seconds | ||
|
||
requester = requester_factory.new( | ||
base_url: "http://#{hostname}:#{port}", | ||
default_headers: DEFAULT_HEADERS, | ||
endpoints: endpoints, | ||
) | ||
|
||
while Time.now < warmup_deadline | ||
requester.call | ||
end | ||
|
||
success = true | ||
ensure | ||
logger.info "Finished warmup work #{success ? 'successfully' : 'with error'}" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
require 'faraday' | ||
|
||
module WarmBlanket | ||
# Issues one request per call to the configured endpoint | ||
class Requester | ||
|
||
InvalidHTTPVerb = Class.new(StandardError) | ||
|
||
private | ||
|
||
SUPPORTED_VERBS = [:get, :post, :put].freeze | ||
private_constant :SUPPORTED_VERBS | ||
|
||
attr_reader :base_url | ||
attr_reader :default_headers | ||
attr_reader :endpoints | ||
attr_reader :logger | ||
attr_reader :connection_factory | ||
|
||
attr_accessor :next_endpoint_position | ||
|
||
public | ||
|
||
def initialize(base_url:, default_headers:, endpoints:, logger: WarmBlanket.config.logger, connection_factory: Faraday) | ||
@base_url = base_url | ||
@default_headers = default_headers | ||
@endpoints = endpoints | ||
@logger = logger | ||
@connection_factory = connection_factory | ||
@next_endpoint_position = 0 | ||
end | ||
|
||
def call | ||
connection = connection_factory.new(url: base_url) | ||
|
||
endpoint = next_endpoint | ||
|
||
http_verb = extract_verb(endpoint) | ||
|
||
logger.debug "Requesting #{endpoint.fetch(http_verb)}" | ||
|
||
response = connection.public_send(http_verb) do |request| | ||
apply_headers(request, default_headers) | ||
apply_headers(request, endpoint[:headers]) | ||
request.url(endpoint.fetch(http_verb)) | ||
request.body = endpoint[:body] if endpoint[:body] | ||
end | ||
|
||
if response.status == 200 | ||
logger.debug "Request successful" | ||
else | ||
logger.warn "Request to #{endpoint.fetch(:get)} failed with code #{response.status}" | ||
end | ||
|
||
nil | ||
end | ||
|
||
private | ||
|
||
def apply_headers(request, headers) | ||
headers&.each do |header, value| | ||
request.headers[header.to_s] = value | ||
end | ||
end | ||
|
||
def next_endpoint | ||
next_endpoint = endpoints[next_endpoint_position] | ||
self.next_endpoint_position = (next_endpoint_position + 1) % endpoints.size | ||
next_endpoint | ||
end | ||
|
||
def extract_verb(endpoint) | ||
SUPPORTED_VERBS.each do |verb| | ||
return verb if endpoint.key?(verb) | ||
end | ||
|
||
raise InvalidHTTPVerb, "Unsupported or missing HTTP verb for request: #{endpoint.inspect}" | ||
end | ||
end | ||
end |
Oops, something went wrong.