synapse 0.0.1 → 0.2.1

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.
@@ -0,0 +1,98 @@
1
+ require_relative "./base"
2
+
3
+ require 'thread'
4
+ require 'resolv'
5
+
6
+ module Synapse
7
+ class DnsWatcher < BaseWatcher
8
+ def start
9
+ @check_interval = @discovery['check_interval'] || 30.0
10
+ @nameserver = @discovery['nameserver']
11
+
12
+ watch
13
+ end
14
+
15
+ private
16
+ def validate_discovery_opts
17
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
18
+ unless @discovery['method'] == 'dns'
19
+ raise ArgumentError, "a non-empty list of servers is required" \
20
+ if @discovery['servers'].empty?
21
+ end
22
+
23
+ def watch
24
+ @watcher = Thread.new do
25
+ last_resolution = resolve_servers
26
+ configure_backends(last_resolution)
27
+ while true
28
+ begin
29
+ start = Time.now
30
+ current_resolution = resolve_servers
31
+ unless last_resolution == current_resolution
32
+ last_resolution = current_resolution
33
+ configure_backends(last_resolution)
34
+ end
35
+
36
+ sleep_until_next_check(start)
37
+ rescue => e
38
+ log.warn "Error in watcher thread: #{e.inspect}"
39
+ log.warn e.backtrace
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def sleep_until_next_check(start_time)
46
+ sleep_time = @check_interval - (Time.now - start_time)
47
+ if sleep_time > 0.0
48
+ sleep(sleep_time)
49
+ end
50
+ end
51
+
52
+ def resolve_servers
53
+ resolver.tap do |dns|
54
+ resolution = @discovery['servers'].map do |server|
55
+ addresses = dns.getaddresses(server['host']).map(&:to_s)
56
+ [server, addresses.sort]
57
+ end
58
+
59
+ return resolution
60
+ end
61
+ rescue => e
62
+ log.warn "Error while resolving host names: #{e.inspect}"
63
+ []
64
+ end
65
+
66
+ def resolver
67
+ args = [{:nameserver => @nameserver}] if @nameserver
68
+ Resolv::DNS.open(*args)
69
+ end
70
+
71
+ def configure_backends(servers)
72
+ new_backends = servers.flat_map do |(server, addresses)|
73
+ addresses.map do |address|
74
+ {
75
+ 'name' => "#{server['name']}-#{[address, server['port']].hash}",
76
+ 'host' => address,
77
+ 'port' => server['port']
78
+ }
79
+ end
80
+ end
81
+
82
+ if new_backends.empty?
83
+ if @default_servers.empty?
84
+ log.warn "synapse: no backends and no default servers for service #{@name};" \
85
+ " using previous backends: #{@backends.inspect}"
86
+ else
87
+ log.warn "synapse: no backends for service #{@name};" \
88
+ " using default servers: #{@default_servers.inspect}"
89
+ @backends = @default_servers
90
+ end
91
+ else
92
+ log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
93
+ @backends = new_backends
94
+ end
95
+ @synapse.configure
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,26 @@
1
+ require_relative "./base"
2
+
3
+ module Synapse
4
+ class EC2Watcher < BaseWatcher
5
+ def start
6
+ # connect to ec2
7
+ # find all servers whose @discovery['tag_name'] matches @discovery['tag_value']
8
+ # call @synapse.configure
9
+ end
10
+
11
+ private
12
+ def validate_discovery_opts
13
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
14
+ unless @discovery['method'] == 'ec2tag'
15
+ raise ArgumentError, "a `server_port_override` option is required for ec2tag watchers" \
16
+ unless @server_port_override
17
+ raise ArgumentError, "missing aws credentials for service #{@name}" \
18
+ unless (@discovery['aws_key'] && @discovery['aws_secret'])
19
+ raise ArgumentError, "aws tag name is required for service #{@name}" \
20
+ unless @discovery['tag_name']
21
+ raise ArgumentError, "aws tag value required for service #{@name}" \
22
+ unless @discovery['tag_value']
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,138 @@
1
+ require_relative "./base"
2
+
3
+ require_relative "../../gen-rb/endpoint_types"
4
+ require_relative "../../gen-rb/thrift"
5
+ require 'zk'
6
+
7
+ module Synapse
8
+ class ZookeeperWatcher < BaseWatcher
9
+ def start
10
+ zk_hosts = @discovery['hosts'].shuffle.join(',')
11
+
12
+ log.info "synapse: starting ZK watcher #{@name} @ hosts: #{zk_hosts}, path: #{@discovery['path']}"
13
+ @zk = ZK.new(zk_hosts)
14
+
15
+ @deserializer = Thrift::Deserializer.new
16
+
17
+ watch
18
+ discover
19
+ end
20
+
21
+ private
22
+ def validate_discovery_opts
23
+ raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
24
+ unless @discovery['method'] == 'zookeeper'
25
+ raise ArgumentError, "missing or invalid zookeeper host for service #{@name}" \
26
+ unless @discovery['hosts']
27
+ raise ArgumentError, "invalid zookeeper path for service #{@name}" \
28
+ unless @discovery['path']
29
+ end
30
+
31
+ # helper method that ensures that the discovery path exists
32
+ def create(path)
33
+ log.debug "synapse: creating ZK path: #{path}"
34
+ # recurse if the parent node does not exist
35
+ create File.dirname(path) unless @zk.exists? File.dirname(path)
36
+ @zk.create(path, ignore: :node_exists)
37
+ end
38
+
39
+ # find the current backends at the discovery path; sets @backends
40
+ def discover
41
+ log.info "synapse: discovering backends for service #{@name}"
42
+
43
+ new_backends = []
44
+ begin
45
+ @zk.children(@discovery['path'], :watch => true).map do |name|
46
+ node = @zk.get("#{@discovery['path']}/#{name}")
47
+
48
+ begin
49
+ host, port = deserialize_service_instance(node.first)
50
+ rescue
51
+ log.error "synapse: invalid data in ZK node #{name} at #{@discovery['path']}"
52
+ else
53
+ server_port = @server_port_override ? @server_port_override : port
54
+ backend_name = "#{name}-#{[host, server_port].hash}"
55
+
56
+ log.debug "synapse: discovered backend #{backend_name} at #{host}:#{server_port} for service #{@name}"
57
+ new_backends << { 'name' => backend_name, 'host' => host, 'port' => server_port}
58
+ end
59
+ end
60
+ rescue ZK::Exceptions::NoNode
61
+ # the path must exist, otherwise watch callbacks will not work
62
+ create(@discovery['path'])
63
+ retry
64
+ end
65
+
66
+ if new_backends.empty?
67
+ if @default_servers.empty?
68
+ log.warn "synapse: no backends and no default servers for service #{@name}; using previous backends: #{@backends.inspect}"
69
+ else
70
+ log.warn "synapse: no backends for service #{@name}; using default servers: #{@default_servers.inspect}"
71
+ @backends = @default_servers
72
+ end
73
+ else
74
+ log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
75
+ @backends = new_backends
76
+ end
77
+ end
78
+
79
+ # sets up zookeeper callbacks if the data at the discovery path changes
80
+ def watch
81
+ @watcher.unsubscribe if defined? @watcher
82
+ @watcher = @zk.register(@discovery['path'], &watcher_callback)
83
+ end
84
+
85
+ # handles the event that a watched path has changed in zookeeper
86
+ def watcher_callback
87
+ Proc.new do |event|
88
+ # Set new watcher
89
+ watch
90
+ # Rediscover
91
+ discover
92
+ # send a message to calling class to reconfigure
93
+ @synapse.configure
94
+ end
95
+ end
96
+
97
+ # tries to extract host/port from a json hash
98
+ def parse_json(data)
99
+ begin
100
+ json = JSON.parse data
101
+ rescue Object => o
102
+ return false
103
+ end
104
+ raise 'instance json data does not have host key' unless json.has_key?('host')
105
+ raise 'instance json data does not have port key' unless json.has_key?('port')
106
+ return json['host'], json['port']
107
+ end
108
+
109
+ # tries to extract a host/port from twitter thrift data
110
+ def parse_thrift(data)
111
+ begin
112
+ service = Twitter::Thrift::ServiceInstance.new
113
+ @deserializer.deserialize(service, data)
114
+ rescue Object => o
115
+ return false
116
+ end
117
+ raise "instance thrift data does not have host" if service.serviceEndpoint.host.nil?
118
+ raise "instance thrift data does not have port" if service.serviceEndpoint.port.nil?
119
+ return service.serviceEndpoint.host, service.serviceEndpoint.port
120
+ end
121
+
122
+ # decode the data at a zookeeper endpoint
123
+ def deserialize_service_instance(data)
124
+ log.debug "synapse: deserializing process data"
125
+
126
+ # first, lets try parsing this as thrift
127
+ host, port = parse_thrift(data)
128
+ return host, port if host
129
+
130
+ # if that does not work, try json
131
+ host, port = parse_json(data)
132
+ return host, port if host
133
+
134
+ # if we got this far, then we have a problem
135
+ raise "could not decode this data:\n#{data}"
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "./service_watcher/base"
2
+ require_relative "./service_watcher/zookeeper"
3
+ require_relative "./service_watcher/ec2tag"
4
+ require_relative "./service_watcher/dns"
5
+
6
+ module Synapse
7
+ class ServiceWatcher
8
+
9
+ @watchers = {
10
+ 'base'=>BaseWatcher,
11
+ 'zookeeper'=>ZookeeperWatcher,
12
+ 'ec2tag'=>EC2Watcher,
13
+ 'dns' => DnsWatcher
14
+ }
15
+
16
+ # the method which actually dispatches watcher creation requests
17
+ def self.create(opts, synapse)
18
+ raise ArgumentError, "Missing discovery method when trying to create watcher" \
19
+ unless opts.has_key?('discovery') && opts['discovery'].has_key?('method')
20
+
21
+ discovery_method = opts['discovery']['method']
22
+ raise ArgumentError, "Invalid discovery method #{discovery_method}" \
23
+ unless @watchers.has_key?(discovery_method)
24
+
25
+ return @watchers[discovery_method].new(opts, synapse)
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.0.1"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/synapse.rb CHANGED
@@ -1,5 +1,64 @@
1
- require "synapse/version"
1
+ require_relative "synapse/version"
2
+ require_relative "synapse/base"
3
+ require_relative "synapse/haproxy"
4
+ require_relative "synapse/service_watcher"
5
+
6
+ require 'logger'
7
+ require 'json'
8
+
9
+ include Synapse
2
10
 
3
11
  module Synapse
4
- # Your code goes here...
12
+ class Synapse
13
+ def initialize(opts={})
14
+ # disable configuration until this is started
15
+ @configure_enabled = false
16
+
17
+ # create the service watchers for all our services
18
+ raise "specify a list of services to connect in the config" unless opts.has_key?('services')
19
+ @service_watchers = create_service_watchers(opts['services'])
20
+
21
+ # create the haproxy object
22
+ raise "haproxy config section is missing" unless opts.has_key?('haproxy')
23
+ @haproxy = Haproxy.new(opts['haproxy'])
24
+ end
25
+
26
+ # start all the watchers and enable haproxy configuration
27
+ def run
28
+ log.info "synapse: starting..."
29
+
30
+ @service_watchers.map { |watcher| watcher.start }
31
+ @configure_enabled = true
32
+ configure
33
+
34
+ # loop forever
35
+ loops = 0
36
+ loop do
37
+ sleep 1
38
+ loops += 1
39
+ log.debug "synapse: still running at #{Time.now}" if (loops % 60) == 0
40
+ end
41
+ end
42
+
43
+ # reconfigure haproxy based on our watchers
44
+ def configure
45
+ if @configure_enabled
46
+ log.info "synapse: regenerating haproxy config"
47
+ @haproxy.update_config(@service_watchers)
48
+ else
49
+ log.info "synapse: reconfigure requested, but it's not yet enabled"
50
+ end
51
+ end
52
+
53
+ private
54
+ def create_service_watchers(services={})
55
+ service_watchers =[]
56
+ services.each do |service_config|
57
+ service_watchers << ServiceWatcher.create(service_config, self)
58
+ end
59
+
60
+ return service_watchers
61
+ end
62
+
63
+ end
5
64
  end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ class MockWatcher; end;
4
+
5
+ describe Synapse::Haproxy do
6
+ subject { Synapse::Haproxy.new(config['haproxy']) }
7
+
8
+ it 'updating the config' do
9
+ mockWatcher = mock(Synapse::ServiceWatcher)
10
+ binding.pry
11
+ subject.should_receive(:generate_config)
12
+ subject.update_config([mockWatcher])
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ require_relative '../lib/synapse'
8
+ require 'pry'
9
+ require_relative 'support/config'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+ config.include Config
16
+
17
+ # Run specs in random order to surface order dependencies. If you find an
18
+ # order dependency and want to debug it, you can fix the order by providing
19
+ # the seed, which is printed after each run.
20
+ # --seed 1234
21
+ config.order = 'random'
22
+ end
@@ -0,0 +1,9 @@
1
+ require "yaml"
2
+
3
+ module Config
4
+
5
+ def config
6
+ @config ||= YAML::load_file(File.join(File.dirname(File.expand_path(__FILE__)), 'minimum.conf.yaml'))
7
+ end
8
+
9
+ end
@@ -0,0 +1,27 @@
1
+ # list the services to connect
2
+ services:
3
+ - name: test
4
+ local_port: 3210
5
+ server_options: test_option
6
+ default_servers:
7
+ - { name: default1, host: localhost, port: 8080}
8
+ discovery:
9
+ method: zookeeper
10
+ path: /airbnb/service/logging/event_collector
11
+ hosts:
12
+ - localhost:2181
13
+ listen:
14
+ - test_option
15
+
16
+
17
+ # settings for haproxy including the global config
18
+ haproxy:
19
+ reload_command: "sudo service haproxy reload"
20
+ config_file_path: "/etc/haproxy/haproxy.cfg"
21
+ do_writes: false
22
+ do_reloads: false
23
+ global:
24
+ - global_test_option
25
+
26
+ defaults:
27
+ - default_test_option
data/synapse.gemspec CHANGED
@@ -16,4 +16,11 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency "zk", "~> 1.7.4"
21
+ gem.add_runtime_dependency "thrift", "~> 0.9.0"
22
+
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "pry"
25
+ gem.add_development_dependency "pry-nav"
19
26
  end
data/test.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash -ex
2
+
3
+ echo running synapse script
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synapse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,23 +9,137 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-09 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-10-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: zk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.7.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.7.4
30
+ - !ruby/object:Gem::Dependency
31
+ name: thrift
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.9.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.9.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: pry-nav
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
14
94
  description: ': Write a gem description'
15
95
  email:
16
96
  - martin.rhoads@airbnb.com
17
- executables: []
97
+ executables:
98
+ - synapse
18
99
  extensions: []
19
100
  extra_rdoc_files: []
20
101
  files:
21
102
  - .gitignore
103
+ - .rspec
22
104
  - Gemfile
105
+ - Gemfile.lock
23
106
  - LICENSE.txt
24
107
  - README.md
25
108
  - Rakefile
109
+ - Vagrantfile
110
+ - bin/synapse
111
+ - chef/converge
112
+ - chef/cookbooks/lxc/recipes/default.rb
113
+ - chef/cookbooks/synapse/attributes/default.rb
114
+ - chef/cookbooks/synapse/recipes/default.rb
115
+ - chef/run.json
116
+ - chef/run.rb
117
+ - client/.RData
118
+ - client/.Rhistory
119
+ - client/bench_rewrite_config.dat
120
+ - client/benchmark-client.iml
121
+ - client/pom.xml
122
+ - client/src/main/java/ClientArsch.java
123
+ - client/src/main/java/META-INF/MANIFEST.MF
124
+ - config/synapse.conf.json
125
+ - haproxy.pid
126
+ - lib/gen-rb/endpoint_types.rb
127
+ - lib/gen-rb/thrift.rb
26
128
  - lib/synapse.rb
129
+ - lib/synapse/base.rb
130
+ - lib/synapse/haproxy.rb
131
+ - lib/synapse/service_watcher.rb
132
+ - lib/synapse/service_watcher/base.rb
133
+ - lib/synapse/service_watcher/dns.rb
134
+ - lib/synapse/service_watcher/ec2tag.rb
135
+ - lib/synapse/service_watcher/zookeeper.rb
27
136
  - lib/synapse/version.rb
137
+ - spec/lib/synapse/haproxy_spec.rb
138
+ - spec/spec_helper.rb
139
+ - spec/support/config.rb
140
+ - spec/support/minimum.conf.yaml
28
141
  - synapse.gemspec
142
+ - test.sh
29
143
  homepage: ''
30
144
  licenses: []
31
145
  post_install_message:
@@ -50,4 +164,8 @@ rubygems_version: 1.8.23
50
164
  signing_key:
51
165
  specification_version: 3
52
166
  summary: ': Write a gem summary'
53
- test_files: []
167
+ test_files:
168
+ - spec/lib/synapse/haproxy_spec.rb
169
+ - spec/spec_helper.rb
170
+ - spec/support/config.rb
171
+ - spec/support/minimum.conf.yaml