activehook-server 0.1.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 +7 -0
- data/.byebug_history +4 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1 -0
- data/Rakefile +10 -0
- data/activehook-server.gemspec +29 -0
- data/bin/activehook-server +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/activehook/server/config.rb +58 -0
- data/lib/activehook/server/errors.rb +12 -0
- data/lib/activehook/server/hook.rb +104 -0
- data/lib/activehook/server/launcher.rb +47 -0
- data/lib/activehook/server/log.rb +31 -0
- data/lib/activehook/server/manager.rb +63 -0
- data/lib/activehook/server/queue.rb +62 -0
- data/lib/activehook/server/redis.rb +19 -0
- data/lib/activehook/server/retry.rb +43 -0
- data/lib/activehook/server/send.rb +74 -0
- data/lib/activehook/server/version.rb +6 -0
- data/lib/activehook/server/worker.rb +78 -0
- data/lib/activehook/server.rb +19 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 84018bc3b4180ed35962e80ada4d065de36bf9b8
|
4
|
+
data.tar.gz: 7c21445511837e69f2297417f759e4cc1bd5ff50
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5bf2dd5799e1b712bf1cc7179dae90239e51b0950d3bc97b8dcd12020da232e05bb99497f1daeff4fd8ba2d88aaac1d5762b66d60b99dfe5f3656e4491e80303
|
7
|
+
data.tar.gz: 1931a161cf1f3c613bf2d6eac816c04857acdec9084b6ec88b3f4165ac8652b335ef5eb87cb2692019067ce664e22058a21950a31b287b635f8a2eb560550f4a
|
data/.byebug_history
ADDED
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at nsweeting@gmail.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Nicholas Sweeting
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# ActiveHook-Server
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activehook/server/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activehook-server"
|
8
|
+
spec.version = ActiveHook::Server::VERSION
|
9
|
+
spec.authors = ["Nicholas Sweeting"]
|
10
|
+
spec.email = ["nsweeting@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "Fast and simple webhook delivery microservice for Ruby."
|
13
|
+
spec.description = "Fast and simple webhook delivery microservice for Ruby."
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.executables = %w( activehook-server )
|
18
|
+
spec.require_paths = %w( lib )
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "redis", "~> 3.3"
|
21
|
+
spec.add_runtime_dependency "connection_pool", "~> 2.2"
|
22
|
+
spec.add_runtime_dependency "puma", "~> 3.4"
|
23
|
+
spec.add_runtime_dependency "rack"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
27
|
+
spec.add_development_dependency "byebug", "~> 5.0"
|
28
|
+
spec.add_development_dependency "fakeredis", "~> 0.5"
|
29
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "activehook/server"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
class << self
|
4
|
+
def configure
|
5
|
+
reset
|
6
|
+
yield(config)
|
7
|
+
end
|
8
|
+
|
9
|
+
def config
|
10
|
+
@config ||= Config.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@config = nil
|
15
|
+
@connection_pool = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Config
|
20
|
+
DEFAULTS = {
|
21
|
+
workers: 2,
|
22
|
+
queue_threads: 2,
|
23
|
+
retry_threads: 1,
|
24
|
+
redis_url: ENV['REDIS_URL'],
|
25
|
+
redis_pool: 5,
|
26
|
+
signature_header: 'X-Webhook-Signature'
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
attr_accessor :workers, :queue_threads, :retry_threads,
|
30
|
+
:redis_url, :redis_pool, :signature_header
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
DEFAULTS.each { |key, value| send("#{key}=", value) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def worker_options
|
37
|
+
{
|
38
|
+
queue_threads: queue_threads,
|
39
|
+
retry_threads: retry_threads
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def manager_options
|
44
|
+
{
|
45
|
+
workers: workers,
|
46
|
+
options: worker_options
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def redis
|
51
|
+
{
|
52
|
+
size: redis_pool,
|
53
|
+
url: redis_url
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
module Errors
|
4
|
+
class Config < StandardError; end
|
5
|
+
class Hook < StandardError; end
|
6
|
+
class HTTP < StandardError; end
|
7
|
+
class Send < StandardError; end
|
8
|
+
class Server < StandardError; end
|
9
|
+
class Worker < StandardError; end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
class Hook
|
4
|
+
attr_accessor :token, :uri, :id, :key, :retry_max, :retry_time, :created_at
|
5
|
+
attr_reader :errors, :payload
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
options = defaults.merge(options)
|
9
|
+
options.each { |key, value| send("#{key}=", value) }
|
10
|
+
@errors = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
return false unless valid?
|
15
|
+
save_hook
|
16
|
+
end
|
17
|
+
|
18
|
+
def save!
|
19
|
+
raise Errors::Hook, 'Hook is invalid' unless valid?
|
20
|
+
save_hook
|
21
|
+
end
|
22
|
+
|
23
|
+
def payload=(payload)
|
24
|
+
if payload.is_a?(String)
|
25
|
+
@payload = JSON.parse(payload)
|
26
|
+
else
|
27
|
+
@payload = payload
|
28
|
+
end
|
29
|
+
rescue JSON::ParserError
|
30
|
+
@payload = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def retry?
|
34
|
+
fail_at > Time.now.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def retry_at
|
38
|
+
Time.now.to_i + @retry_time.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
def fail_at
|
42
|
+
@created_at.to_i + retry_max_time
|
43
|
+
end
|
44
|
+
|
45
|
+
def retry_max_time
|
46
|
+
@retry_time.to_i * @retry_max.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_json
|
50
|
+
{ id: @id,
|
51
|
+
key: @key,
|
52
|
+
token: @token,
|
53
|
+
created_at: @created_at,
|
54
|
+
retry_time: @retry_time,
|
55
|
+
retry_max: @retry_max,
|
56
|
+
uri: @uri,
|
57
|
+
payload: @payload }.to_json
|
58
|
+
end
|
59
|
+
|
60
|
+
def final_payload
|
61
|
+
{ hook_id: @id,
|
62
|
+
hook_key: @key,
|
63
|
+
hook_time: @created_at,
|
64
|
+
hook_signature: ActiveHook.config.signature_header,
|
65
|
+
payload: @payload }.to_json
|
66
|
+
end
|
67
|
+
|
68
|
+
def signature
|
69
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), @token, final_payload)
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid?
|
73
|
+
validate!
|
74
|
+
@errors.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def save_hook
|
80
|
+
ActiveHook::Server.redis.with do |conn|
|
81
|
+
@id = conn.incr('ah:total_queued')
|
82
|
+
conn.lpush('ah:queue', to_json)
|
83
|
+
conn.zadd('ah:validation', @id, @key)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def defaults
|
88
|
+
{ key: SecureRandom.uuid,
|
89
|
+
created_at: Time.now.to_i,
|
90
|
+
retry_time: 3600,
|
91
|
+
retry_max: 3 }
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate!
|
95
|
+
@errors.merge!(token: ['must be a string.']) unless @token.is_a?(String)
|
96
|
+
@errors.merge!(payload: ['must be a Hash']) unless @payload.is_a?(Hash)
|
97
|
+
@errors.merge!(uri: ['is not a valid format.']) unless @uri =~ /\A#{URI::regexp}\z/
|
98
|
+
@errors.merge!(created_at: ['must be an Integer.']) unless @created_at.is_a?(Integer)
|
99
|
+
@errors.merge!(retry_time: ['must be an Integer.']) unless @retry_time.is_a?(Integer)
|
100
|
+
@errors.merge!(retry_max: ['must be an Integer.']) unless @retry_max.is_a?(Integer)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
# Handles the start of the ActiveHook server via command line
|
4
|
+
#
|
5
|
+
class Launcher
|
6
|
+
def initialize(argv)
|
7
|
+
@argv = argv
|
8
|
+
end
|
9
|
+
|
10
|
+
# Parses commmand line options and starts the Manager object
|
11
|
+
#
|
12
|
+
def start
|
13
|
+
start_message
|
14
|
+
setup_options
|
15
|
+
boot_manager
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def start_message
|
21
|
+
ActiveHook::Server.log.info('ActiveHook Server starting!')
|
22
|
+
ActiveHook::Server.log.info("* Version #{VERSION}, codename: #{CODENAME}")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses the arguments passed through the command line.
|
26
|
+
#
|
27
|
+
def setup_options
|
28
|
+
parser = OptionParser.new do |o|
|
29
|
+
o.banner = 'Usage: bundle exec bin/activehook [options]'
|
30
|
+
|
31
|
+
o.on('-c', '--config PATH', 'Load PATH for config file') do |arg|
|
32
|
+
load(arg)
|
33
|
+
ActiveHook::Server.log.info("* Server config: #{arg}")
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on('-h', '--help', 'Prints this help') { puts o && exit }
|
37
|
+
end
|
38
|
+
parser.parse!(@argv)
|
39
|
+
end
|
40
|
+
|
41
|
+
def boot_manager
|
42
|
+
manager = Manager.new(ActiveHook::Server.config.manager_options)
|
43
|
+
manager.start
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module ActiveHook
|
4
|
+
module Server
|
5
|
+
class << self
|
6
|
+
STDOUT.sync = true
|
7
|
+
|
8
|
+
def log
|
9
|
+
@log ||= Log.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Log
|
14
|
+
def initialize
|
15
|
+
@log = ::Logger.new(STDOUT)
|
16
|
+
@log.formatter = proc do |_severity, datetime, _progname, msg|
|
17
|
+
"#{msg}\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def info(msg)
|
22
|
+
@log.info("[ \e[32mOK\e[0m ] #{msg}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def err(msg, action: :no_exit)
|
26
|
+
@log.info("[ \e[31mER\e[0m ] #{msg}")
|
27
|
+
exit 1 if action == :exit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
# The Manager controls our Worker processes. We use it to instruct each
|
4
|
+
# of them to start and shutdown.
|
5
|
+
#
|
6
|
+
class Manager
|
7
|
+
attr_accessor :workers, :options
|
8
|
+
attr_reader :forks
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
options.each { |key, value| send("#{key}=", value) }
|
12
|
+
@master = Process.pid
|
13
|
+
at_exit { shutdown }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Instantiates new Worker objects, setting them with our options. We
|
17
|
+
# follow up by booting each of our Workers. Our Manager is then put to
|
18
|
+
# sleep so that our Workers can do their thing.
|
19
|
+
#
|
20
|
+
def start
|
21
|
+
validate!
|
22
|
+
start_messages
|
23
|
+
create_workers
|
24
|
+
Process.wait
|
25
|
+
end
|
26
|
+
|
27
|
+
# Shutsdown our Worker processes.
|
28
|
+
#
|
29
|
+
def shutdown
|
30
|
+
@forks.each { |w| Process.kill('SIGINT', w[:pid].to_i) }
|
31
|
+
Process.kill('SIGINT', @master)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Create the specified number of workers and starts them
|
37
|
+
#
|
38
|
+
def create_workers
|
39
|
+
@forks = []
|
40
|
+
@workers.times do |id|
|
41
|
+
pid = fork { Worker.new(@options.merge(id: id)).start }
|
42
|
+
@forks << { id: id, pid: pid }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Information about the start process
|
47
|
+
#
|
48
|
+
def start_messages
|
49
|
+
ActiveHook::Server.log.info("* Workers: #{@workers}")
|
50
|
+
ActiveHook::Server.log.info("* Threads: #{@options[:queue_threads]} queue, #{@options[:retry_threads]} retry")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validates our data before starting our Workers. Also instantiates our
|
54
|
+
# connection pool by pinging Redis.
|
55
|
+
#
|
56
|
+
def validate!
|
57
|
+
raise Errors::Server, 'Cound not connect to Redis.' unless ActiveHook::Server.redis.with { |c| c.ping && c.quit }
|
58
|
+
raise Errors::Server, 'Workers must be an Integer.' unless @workers.is_a?(Integer)
|
59
|
+
raise Errors::Server, 'Options must be a Hash.' unless @options.is_a?(Hash)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
# The Queue object processes any hooks that are queued into our Redis server.
|
4
|
+
# It will perform a 'blocking pop' on our hook list until one is added.
|
5
|
+
#
|
6
|
+
class Queue
|
7
|
+
def initialize
|
8
|
+
@done = false
|
9
|
+
end
|
10
|
+
|
11
|
+
# Starts our queue process. This will run until instructed to stop.
|
12
|
+
#
|
13
|
+
def start
|
14
|
+
until @done
|
15
|
+
json = retrieve_hook
|
16
|
+
HookRunner.new(json) if json
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Shutsdown our queue process.
|
21
|
+
#
|
22
|
+
def shutdown
|
23
|
+
@done = true
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Performs a 'blocking pop' on our redis queue list.
|
29
|
+
#
|
30
|
+
def retrieve_hook
|
31
|
+
json = ActiveHook::Server.redis.with { |c| c.brpop('ah:queue') }
|
32
|
+
json.last if json
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class HookRunner
|
37
|
+
def initialize(json)
|
38
|
+
@hook = Hook.new(JSON.parse(json))
|
39
|
+
@post = Send.new(hook: @hook)
|
40
|
+
start
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
@post.start
|
45
|
+
ActiveHook::Server.redis.with do |conn|
|
46
|
+
@post.success? ? hook_success(conn) : hook_failed(conn)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def hook_success(conn)
|
53
|
+
conn.incr('ah:total_success')
|
54
|
+
end
|
55
|
+
|
56
|
+
def hook_failed(conn)
|
57
|
+
conn.zadd('ah:retry', @hook.retry_at, @hook.to_json) if @hook.retry?
|
58
|
+
conn.incr('ah:total_failed')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
class << self
|
4
|
+
attr_reader :connection_pool
|
5
|
+
|
6
|
+
def redis
|
7
|
+
@connection_pool ||= ConnectionPool.create
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ConnectionPool
|
12
|
+
def self.create
|
13
|
+
::ConnectionPool.new(size: ActiveHook::Server.config.redis_pool) do
|
14
|
+
Redis.new(url: ActiveHook::Server.config.redis_url)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
class Retry
|
4
|
+
def initialize
|
5
|
+
@done = false
|
6
|
+
end
|
7
|
+
|
8
|
+
def start
|
9
|
+
until @done
|
10
|
+
ActiveHook::Server.redis.with do |conn|
|
11
|
+
conn.watch('ah:retry') do
|
12
|
+
retries = retrieve_retries(conn)
|
13
|
+
update_retries(conn, retries)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
sleep 2
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def shutdown
|
21
|
+
@done = true
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def retrieve_retries(conn)
|
27
|
+
conn.zrangebyscore('ah:retry', 0, Time.now.to_i)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_retries(conn, retries)
|
31
|
+
if retries.any?
|
32
|
+
conn.multi do |multi|
|
33
|
+
multi.incrby('ah:total_retries', retries.count)
|
34
|
+
multi.zrem('ah:retry', retries)
|
35
|
+
multi.lpush('ah:queue', retries)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
conn.unwatch
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
class Send
|
4
|
+
REQUEST_HEADERS = {
|
5
|
+
"Content-Type" => "application/json",
|
6
|
+
"Accept" => "application/json",
|
7
|
+
"User-Agent" => "ActiveHook/#{VERSION}"
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
attr_accessor :hook
|
11
|
+
attr_reader :response_time, :status, :response
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
options.each { |key, value| send("#{key}=", value) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@status = post_hook
|
19
|
+
log_status
|
20
|
+
end
|
21
|
+
|
22
|
+
def uri
|
23
|
+
@uri ||= URI.parse(@hook.uri)
|
24
|
+
end
|
25
|
+
|
26
|
+
def success?
|
27
|
+
@status == :success
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def post_hook
|
33
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
34
|
+
measure_response_time do
|
35
|
+
@response = http.post(uri.path, @hook.final_payload, final_headers)
|
36
|
+
end
|
37
|
+
response_status(@response)
|
38
|
+
rescue
|
39
|
+
:error
|
40
|
+
end
|
41
|
+
|
42
|
+
def measure_response_time
|
43
|
+
start = Time.now
|
44
|
+
yield
|
45
|
+
finish = Time.now
|
46
|
+
@response_time = "| #{((finish - start) * 1000.0).round(3)} ms"
|
47
|
+
end
|
48
|
+
|
49
|
+
def response_status(response)
|
50
|
+
case response.code.to_i
|
51
|
+
when (200..204)
|
52
|
+
:success
|
53
|
+
when (400..499)
|
54
|
+
:bad_request
|
55
|
+
when (500..599)
|
56
|
+
:server_problems
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def log_status
|
61
|
+
msg = "POST | #{uri} | #{status.upcase} #{response_time}"
|
62
|
+
if status == :success
|
63
|
+
ActiveHook::Server.log.info(msg)
|
64
|
+
else
|
65
|
+
ActiveHook::Server.log.err(msg)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def final_headers
|
70
|
+
{ "X-Hook-Signature" => @hook.signature }.merge(REQUEST_HEADERS)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module ActiveHook
|
2
|
+
module Server
|
3
|
+
# The Worker manages our two main processes - Queue and Retry. Each of these
|
4
|
+
# processes is alloted a number of threads. These threads are then forked.
|
5
|
+
# Each worker object maintains control of these threads through the aptly
|
6
|
+
# named start and shutdown methods.
|
7
|
+
#
|
8
|
+
class Worker
|
9
|
+
attr_accessor :queue_threads, :retry_threads, :id
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
options.each { |key, value| send("#{key}=", value) }
|
13
|
+
@pid = Process.pid
|
14
|
+
@threads = []
|
15
|
+
@_threads_real = []
|
16
|
+
at_exit { shutdown }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Starts our new worker.
|
20
|
+
#
|
21
|
+
def start
|
22
|
+
validate!
|
23
|
+
start_message
|
24
|
+
build_threads
|
25
|
+
start_threads
|
26
|
+
end
|
27
|
+
|
28
|
+
# Shutsdown our worker as well as its threads.
|
29
|
+
#
|
30
|
+
def shutdown
|
31
|
+
shutdown_message
|
32
|
+
@threads.each(&:shutdown)
|
33
|
+
@_threads_real.each(&:exit)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Forks the worker and creates the actual threads (@_threads_real) for
|
39
|
+
# our Queue and Retry objects. We then start them and join them to the
|
40
|
+
# main process.
|
41
|
+
#
|
42
|
+
def start_threads
|
43
|
+
@threads.each do |thread|
|
44
|
+
@_threads_real << Thread.new { thread.start }
|
45
|
+
end
|
46
|
+
@_threads_real.map(&:join)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Instantiates our Queue and Retry objects based on the number of threads
|
50
|
+
# specified for each process type. We store these objects as an array in
|
51
|
+
# @threads.
|
52
|
+
#
|
53
|
+
def build_threads
|
54
|
+
@queue_threads.times { @threads << Queue.new }
|
55
|
+
@retry_threads.times { @threads << Retry.new }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Information about the start process
|
59
|
+
#
|
60
|
+
def start_message
|
61
|
+
ActiveHook::Server.log.info("* Worker #{@id} started, pid: #{@pid}")
|
62
|
+
end
|
63
|
+
|
64
|
+
# Information about the shutdown process
|
65
|
+
#
|
66
|
+
def shutdown_message
|
67
|
+
ActiveHook::Server.log.info("* Worker #{@id} shutdown, pid: #{@pid}")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Validates our data before starting the worker.
|
71
|
+
#
|
72
|
+
def validate!
|
73
|
+
raise Errors::Worker, 'Queue threads must be an Integer.' unless @queue_threads.is_a?(Integer)
|
74
|
+
raise Errors::Worker, 'Retry threads must be an Integer.' unless @retry_threads.is_a?(Integer)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'byebug'
|
2
|
+
require 'redis'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'openssl'
|
7
|
+
require 'connection_pool'
|
8
|
+
require 'activehook/server/hook'
|
9
|
+
require 'activehook/server/config'
|
10
|
+
require 'activehook/server/redis'
|
11
|
+
require 'activehook/server/errors'
|
12
|
+
require 'activehook/server/log'
|
13
|
+
require 'activehook/server/config'
|
14
|
+
require 'activehook/server/launcher'
|
15
|
+
require 'activehook/server/manager'
|
16
|
+
require 'activehook/server/queue'
|
17
|
+
require 'activehook/server/retry'
|
18
|
+
require 'activehook/server/send'
|
19
|
+
require 'activehook/server/worker'
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activehook-server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicholas Sweeting
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: connection_pool
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: puma
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.12'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.12'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '5.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '5.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '5.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '5.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: fakeredis
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.5'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.5'
|
139
|
+
description: Fast and simple webhook delivery microservice for Ruby.
|
140
|
+
email:
|
141
|
+
- nsweeting@gmail.com
|
142
|
+
executables:
|
143
|
+
- activehook-server
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".byebug_history"
|
148
|
+
- ".gitignore"
|
149
|
+
- ".travis.yml"
|
150
|
+
- CODE_OF_CONDUCT.md
|
151
|
+
- Gemfile
|
152
|
+
- LICENSE.txt
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- activehook-server.gemspec
|
156
|
+
- bin/activehook-server
|
157
|
+
- bin/console
|
158
|
+
- bin/setup
|
159
|
+
- lib/activehook/server.rb
|
160
|
+
- lib/activehook/server/config.rb
|
161
|
+
- lib/activehook/server/errors.rb
|
162
|
+
- lib/activehook/server/hook.rb
|
163
|
+
- lib/activehook/server/launcher.rb
|
164
|
+
- lib/activehook/server/log.rb
|
165
|
+
- lib/activehook/server/manager.rb
|
166
|
+
- lib/activehook/server/queue.rb
|
167
|
+
- lib/activehook/server/redis.rb
|
168
|
+
- lib/activehook/server/retry.rb
|
169
|
+
- lib/activehook/server/send.rb
|
170
|
+
- lib/activehook/server/version.rb
|
171
|
+
- lib/activehook/server/worker.rb
|
172
|
+
homepage:
|
173
|
+
licenses:
|
174
|
+
- MIT
|
175
|
+
metadata: {}
|
176
|
+
post_install_message:
|
177
|
+
rdoc_options: []
|
178
|
+
require_paths:
|
179
|
+
- lib
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">="
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
requirements: []
|
191
|
+
rubyforge_project:
|
192
|
+
rubygems_version: 2.5.0
|
193
|
+
signing_key:
|
194
|
+
specification_version: 4
|
195
|
+
summary: Fast and simple webhook delivery microservice for Ruby.
|
196
|
+
test_files: []
|
197
|
+
has_rdoc:
|