synapse 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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