dat-tcp 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.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tcp_server.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'bench/tasks'
4
+
5
+ require "assert/rake_tasks"
6
+ Assert::RakeTasks.install
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dat-tcp/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dat-tcp"
8
+ gem.version = DatTCP::VERSION
9
+ gem.authors = ["Collin Redding"]
10
+ gem.email = ["collin.redding@me.com"]
11
+ gem.description = "DatTCP is a generic threaded TCP server implementation. It provides a " \
12
+ "simple to use interface for defining a TCP server. It is intended to be " \
13
+ "used as a base for application-level servers."
14
+ gem.summary = "DatTCP is a generic threaded TCP server for defining application-level " \
15
+ "servers."
16
+ gem.homepage = ""
17
+
18
+ gem.files = `git ls-files -- lib/* Gemfile Rakefile *.gemspec`.split($/)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ["lib"]
22
+
23
+ gem.add_development_dependency('assert', ['~>0.8'])
24
+ gem.add_development_dependency('assert-mocha', ['~>0.1'])
25
+ end
@@ -0,0 +1,158 @@
1
+ # DatTCP Server module is the main interface for defining a new server. It
2
+ # should be mixed in and provides methods for starting and stopping the main
3
+ # server loop. The `serve` method is intended to be overwritten so users can
4
+ # define handling connections. It's primary loop is:
5
+ #
6
+ # 1. Wait for worker
7
+ # 1. Accept connection
8
+ # 2. Process connection by handing off to worker
9
+ #
10
+ # This is repeated until the server is stopped.
11
+ #
12
+ # Options:
13
+ # `max_workers` - (integer) The maximum number of workers for processing
14
+ # connections. More threads causes more concurrency but also
15
+ # more overhead. This defaults to 4 workers.
16
+ # `debug` - (boolean) Whether or not to have the server log debug
17
+ # messages for when the server starts and stops and when a
18
+ # client connects and disconnects.
19
+ # `ready_timeout` - (float) The timeout used with `IO.select` waiting for a
20
+ # connection to occur. This can be set to 0 to not wait at
21
+ # all. Defaults to 1 (second).
22
+ #
23
+ require 'logger'
24
+ require 'socket'
25
+ require 'thread'
26
+
27
+ require 'dat-tcp/logger'
28
+ require 'dat-tcp/workers'
29
+ require 'dat-tcp/version'
30
+
31
+ module DatTCP
32
+
33
+ module Server
34
+ attr_reader :host, :port, :workers, :debug, :logger, :ready_timeout
35
+ attr_reader :tcp_server, :thread
36
+
37
+ def initialize(host, port, options = nil)
38
+ options ||= {}
39
+ options[:max_workers] ||= 4
40
+
41
+ @host, @port = [ host, port ]
42
+ @logger = DatTCP::Logger.new(options[:debug])
43
+ @workers = DatTCP::Workers.new(options[:max_workers], self.logger)
44
+ @ready_timeout = options[:ready_timeout] || 1
45
+
46
+ @mutex = Mutex.new
47
+ @condition_variable = ConditionVariable.new
48
+ end
49
+
50
+ def start
51
+ if !self.running?
52
+ @shutdown = false
53
+ !!self.start_server_thread
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def stop
60
+ if self.running?
61
+ @shutdown = true
62
+ @mutex.synchronize do
63
+ while self.thread
64
+ @condition_variable.wait(@mutex)
65
+ end
66
+ end
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def join_thread(limit = nil)
73
+ @thread.join(limit) if self.running?
74
+ end
75
+
76
+ def running?
77
+ !!@thread
78
+ end
79
+
80
+ # This method should be overwritten to handle new connections
81
+ def serve(socket)
82
+ end
83
+
84
+ def name
85
+ "#{self.class}|#{self.host}:#{self.port}"
86
+ end
87
+
88
+ def inspect
89
+ reference = '0x0%x' % (self.object_id << 1)
90
+ "#<#{self.class}:#{reference} @host=#{self.host.inspect} @port=#{self.port.inspect}>"
91
+ end
92
+
93
+ protected
94
+
95
+ def start_server_thread
96
+ @tcp_server = TCPServer.new(self.host, self.port)
97
+ @mutex.synchronize do
98
+ @thread = Thread.new{ self.work_loop }
99
+ end
100
+ end
101
+
102
+ # Notes:
103
+ # * If the server has been shutdown, then `accept_connection` will return
104
+ # `nil` always. This will exit the loop and begin shutting down the server.
105
+ def work_loop
106
+ self.logger.info("Starting...")
107
+ while !@shutdown
108
+ connection = self.accept_connection
109
+ self.workers.process(connection){|client| self.serve(client) } if connection
110
+ end
111
+ rescue Exception => exception
112
+ self.logger.info("Exception occurred, stopping server!")
113
+ ensure
114
+ self.shutdown_server_thread(exception)
115
+ end
116
+
117
+ # This method is a accept-loop waiting for a new connection. It uses
118
+ # `IO.select` with a timeout to wait for a socket to be ready. Once the
119
+ # socket is ready, it calls `accept` and returns the connection. If the
120
+ # server socket doesn't have a new connection waiting, the loop starts over.
121
+ # In the case the server has been shutdown, it will also break out of the
122
+ # loop.
123
+ #
124
+ # Notes:
125
+ # * If the server has been shutdown this will return `nil`.
126
+ def accept_connection
127
+ loop do
128
+ if IO.select([ @tcp_server ], nil, nil, self.ready_timeout)
129
+ return @tcp_server.accept
130
+ elsif @shutdown
131
+ return
132
+ end
133
+ end
134
+ end
135
+
136
+ # Notes:
137
+ # * Stopping the workers is a graceful shutdown. It will let them each finish
138
+ # processing by joining their threads.
139
+ def shutdown_server_thread(exception = nil)
140
+ self.logger.info("Stopping...")
141
+ @tcp_server.close rescue false
142
+ self.logger.info(" letting any running workers finish...")
143
+ self.workers.finish
144
+ self.logger.info("Stopped")
145
+ if exception
146
+ self.logger.error("#{exception.class}: #{exception.message}")
147
+ self.logger.error(exception.backtrace.join("\n"))
148
+ end
149
+ @mutex.synchronize do
150
+ @thread = nil
151
+ @condition_variable.signal
152
+ end
153
+ true
154
+ end
155
+
156
+ end
157
+
158
+ end
@@ -0,0 +1,36 @@
1
+ # DatTCP's logger module acts as a generator for either a debug or null logger.
2
+ # This allows the server and workers to always assume they have some logger
3
+ # object and not have to worry about conditionally checking if a logger is
4
+ # present. The null logger takes all messages and does nothing with them. When
5
+ # debug mode is turned off, this logger is used, which keeps the server from
6
+ # logging. The debug logger uses an instance of ruby's standard logger and
7
+ # writes to STDOUT.
8
+ #
9
+ require 'logger'
10
+
11
+ module DatTCP::Logger
12
+
13
+ def self.new(debug)
14
+ !!debug ? DatTCP::Logger::Debug.new : DatTCP::Logger::Null.new
15
+ end
16
+
17
+ module Debug
18
+
19
+ def self.new
20
+ ::Logger.new(STDOUT).tap do |logger|
21
+ logger.progname = "[#{self.name}]"
22
+ logger.datetime_format = "%m/%d/%Y %H:%M:%S%p "
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ class Null
29
+
30
+ Logger::Severity.constants.each do |name|
31
+ define_method(name.downcase){|*args| } # no-op
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,3 @@
1
+ module DatTCP
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,99 @@
1
+ # DatTCP's workers is a class for managing the worker threads that are
2
+ # spun up to handle clients. It manages a list of working threads and provides
3
+ # external methods for working with them. Working threads are managed by
4
+ # creating a new one when `process` is called. A client connection and a block
5
+ # are passed to the worker thread for it to handle the connection. Once it's
6
+ # done handling the thread, the connection is closed and the thread is removed
7
+ # from the list. This iignals some of the other methods that the workers class
8
+ # provides that wait on threads to finish.
9
+ #
10
+ require 'thread'
11
+
12
+ require 'dat-tcp/logger'
13
+
14
+ module DatTCP
15
+
16
+ class Workers
17
+ attr_reader :max, :list, :logger
18
+
19
+ def initialize(max = 1, logger = nil)
20
+ @max = max
21
+ @logger = logger || DatTCP::Logger::Null.new
22
+ @list = []
23
+
24
+ @mutex = Mutex.new
25
+ @condition_variable = ConditionVariable.new
26
+ end
27
+
28
+ # This uses the mutex and condition variable to sleep the current thread
29
+ # until signaled. When a thread closes down, it signals, causing this thread
30
+ # to wakeup. This will run the loop again, and as long as a worker is
31
+ # available, the method will return. Otherwise it will sleep again waiting
32
+ # to be signaled.
33
+ def wait_for_available
34
+ @mutex.synchronize do
35
+ while self.list.size >= self.max
36
+ @condition_variable.wait(@mutex)
37
+ end
38
+ end
39
+ end
40
+
41
+ def process(connecting_socket, &block)
42
+ self.wait_for_available
43
+ worker_id = self.list.size + 1
44
+ @list << Thread.new{ self.serve_client(worker_id, connecting_socket, &block) }
45
+ end
46
+
47
+ # Finish simply sleeps the current thread until signaled. Again, when a
48
+ # worker thread closes down, it signals. This will cause this to wake up and
49
+ # continue running the loop. Once the list is empty, the method will return.
50
+ # Otherwise this will sleep until signaled again. This is a graceful
51
+ # shutdown, letting the threads finish their processing.
52
+ def finish
53
+ @mutex.synchronize do
54
+ @list.reject!{|thread| !thread.alive? }
55
+ while !self.list.empty?
56
+ @condition_variable.wait(@mutex)
57
+ end
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def log(message, worker_id)
64
+ self.logger.info("[Worker##{worker_id}] #{message}") if self.logger
65
+ end
66
+
67
+ def serve_client(worker_id, connecting_socket, &block)
68
+ begin
69
+ Thread.current["client_address"] = connecting_socket.peeraddr[1, 2].reverse.join(':')
70
+ self.log("Connecting #{Thread.current["client_address"]}", worker_id)
71
+ block.call(connecting_socket)
72
+ rescue Exception => exception
73
+ self.log("Exception occurred, stopping worker", worker_id)
74
+ ensure
75
+ self.disconnect_client(worker_id, connecting_socket, exception)
76
+ end
77
+ end
78
+
79
+ # Closes the client connection and also shuts the thread down. This is done
80
+ # by removing the thread from the list. This is wrapped in a mutex
81
+ # synchronize, to ensure only one thread interacts with list at a time. Also
82
+ # the condition variable is signaled to trigger the `finish` or
83
+ # `wait_for_available` methods.
84
+ def disconnect_client(worker_id, connecting_socket, exception)
85
+ connecting_socket.close rescue false
86
+ @mutex.synchronize do
87
+ @list.delete(Thread.current)
88
+ @condition_variable.signal
89
+ end
90
+ if exception
91
+ self.log("#{exception.class}: #{exception.message}", worker_id)
92
+ self.log(exception.backtrace.join("\n"), worker_id)
93
+ end
94
+ self.log("Disconnecting #{Thread.current["client_address"]}", worker_id)
95
+ end
96
+
97
+ end
98
+
99
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dat-tcp
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Collin Redding
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-11-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ hash: 27
28
+ segments:
29
+ - 0
30
+ - 8
31
+ version: "0.8"
32
+ requirement: *id001
33
+ name: assert
34
+ type: :development
35
+ - !ruby/object:Gem::Dependency
36
+ prerelease: false
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 9
43
+ segments:
44
+ - 0
45
+ - 1
46
+ version: "0.1"
47
+ requirement: *id002
48
+ name: assert-mocha
49
+ type: :development
50
+ description: DatTCP is a generic threaded TCP server implementation. It provides a simple to use interface for defining a TCP server. It is intended to be used as a base for application-level servers.
51
+ email:
52
+ - collin.redding@me.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - Gemfile
61
+ - Rakefile
62
+ - dat-tcp.gemspec
63
+ - lib/dat-tcp.rb
64
+ - lib/dat-tcp/logger.rb
65
+ - lib/dat-tcp/version.rb
66
+ - lib/dat-tcp/workers.rb
67
+ homepage: ""
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.15
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: DatTCP is a generic threaded TCP server for defining application-level servers.
100
+ test_files: []
101
+