-
Notifications
You must be signed in to change notification settings - Fork 150
Add socket source #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add socket source #227
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4272cf7
Add docket source
4a5ea0f
Add HTTP server source
72cfcac
Same, but with TCPServer
e28ce59
Import for older tornado
6d33f49
Merge branch 'master' into sockets
0421353
flake
f3436d7
add small sleeps to run loop
7b2f3c0
flake again
dc3d801
fix coverage
b76aa14
remove from_socket; add async test
e116639
Responses to feedback
8b798ac
tcp_kwargs to server_kwargs
f453a96
Move wait to becfore shutdown
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ Sources | |
| filenames | ||
| from_kafka | ||
| from_textfile | ||
| from_socket | ||
|
|
||
| DaskStream | ||
| ---------- | ||
|
|
||
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -147,6 +147,142 @@ def do_poll(self): | |
| break | ||
|
|
||
|
|
||
| @Stream.register_api(staticmethod) | ||
| class from_tcp(Source): | ||
| """ | ||
| Creates events by reading from a socket using tornado TCPServer | ||
|
|
||
| The stream of incoming bytes is split on a given delimiter, and the parts | ||
| become the emitted events. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| port : int | ||
| The port to open and listen on. It only gets opened when the source | ||
| is started, and closed upon ``stop()`` | ||
| delimiter : bytes | ||
| The incoming data will be split on this value. The resulting events | ||
| will still have the delimiter at the end. | ||
| start : bool | ||
| Whether to immediately initiate the source. You probably want to | ||
| set up downstream nodes first. | ||
| server_kwargs : dict or None | ||
| If given, additional arguments to pass to TCPServer | ||
|
|
||
| Example | ||
| ------- | ||
|
|
||
| >>> source = Source.from_tcp(4567) # doctest: +SKIP | ||
| """ | ||
| def __init__(self, port, delimiter=b'\n', start=False, | ||
| server_kwargs=None): | ||
| super(from_tcp, self).__init__(ensure_io_loop=True) | ||
| self.stopped = True | ||
| self.server_kwargs = server_kwargs or {} | ||
| self.port = port | ||
| self.server = None | ||
| self.delimiter = delimiter | ||
| if start: # pragma: no cover | ||
| self.start() | ||
|
|
||
| @gen.coroutine | ||
| def _start_server(self): | ||
| from tornado.tcpserver import TCPServer | ||
| from tornado.iostream import StreamClosedError | ||
|
|
||
| class EmitServer(TCPServer): | ||
| source = self | ||
|
|
||
| @gen.coroutine | ||
| def handle_stream(self, stream, address): | ||
| while True: | ||
| try: | ||
| data = yield stream.read_until(self.source.delimiter) | ||
| yield self.source._emit(data) | ||
| except StreamClosedError: | ||
| break | ||
|
|
||
| self.server = EmitServer(**self.server_kwargs) | ||
| self.server.listen(self.port) | ||
|
|
||
| def start(self): | ||
| if self.stopped: | ||
| self.loop.add_callback(self._start_server) | ||
| self.stopped = False | ||
|
|
||
| def stop(self): | ||
| if not self.stopped: | ||
| self.server.stop() | ||
| self.server = None | ||
| self.stopped = True | ||
|
|
||
|
|
||
| @Stream.register_api(staticmethod) | ||
| class from_http_server(Source): | ||
| """Listen for HTTP POSTs on given port | ||
|
|
||
| Each connection will emit one event, containing the body data of | ||
| the request | ||
|
|
||
| Parameters | ||
| ---------- | ||
| port : int | ||
| The port to listen on | ||
| path : str | ||
| Specific path to listen on. Can be regex, but content is not used. | ||
| start : bool | ||
| Whether to immediately startup the server. Usually you want to connect | ||
| downstream nodes first, and then call ``.start()``. | ||
| server_kwargs : dict or None | ||
| If given, set of further parameters to pass on to HTTPServer | ||
|
|
||
| Example | ||
| ------- | ||
| >>> source = Source.from_http_server(4567) # doctest: +SKIP | ||
| """ | ||
|
|
||
| def __init__(self, port, path='/.*', start=False, server_kwargs=None): | ||
| self.port = port | ||
| self.path = path | ||
| self.server_kwargs = server_kwargs or {} | ||
| super(from_http_server, self).__init__(ensure_io_loop=True) | ||
| self.stopped = True | ||
| self.server = None | ||
| if start: # pragma: no cover | ||
| self.start() | ||
|
|
||
| def _start_server(self): | ||
| from tornado.web import Application, RequestHandler | ||
| from tornado.httpserver import HTTPServer | ||
|
|
||
| class Handler(RequestHandler): | ||
| source = self | ||
|
|
||
| @gen.coroutine | ||
| def post(self): | ||
| yield self.source._emit(self.request.body) | ||
| self.write('OK') | ||
|
|
||
| application = Application([ | ||
| (self.path, Handler), | ||
| ]) | ||
| self.server = HTTPServer(application, **self.server_kwargs) | ||
| self.server.listen(self.port) | ||
|
|
||
| def start(self): | ||
| """Start HTTP server and listen""" | ||
| if self.stopped: | ||
| self.loop.add_callback(self._start_server) | ||
| self.stopped = False | ||
|
|
||
| def stop(self): | ||
| """Shutdown HTTP server""" | ||
| if not self.stopped: | ||
| self.server.stop() | ||
| self.server = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You remove the server here, but not in the TCP solution. Is there a reason? |
||
| self.stopped = True | ||
|
|
||
|
|
||
| @Stream.register_api(staticmethod) | ||
| class from_kafka(Source): | ||
| """ Accepts messages from Kafka | ||
|
|
||
This file contains hidden or 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,83 @@ | ||
| import pytest | ||
| from streamz import Source | ||
| from streamz.utils_test import wait_for, await_for, gen_test | ||
| import socket | ||
|
|
||
|
|
||
| def test_tcp(): | ||
| port = 9876 | ||
| s = Source.from_tcp(port) | ||
| out = s.sink_to_list() | ||
| s.start() | ||
| wait_for(lambda: s.server is not None, 2, period=0.02) | ||
|
|
||
| try: | ||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock.connect(("localhost", port)) | ||
| sock.send(b'data\n') | ||
| sock.close() | ||
|
|
||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock.connect(("localhost", port)) | ||
| sock.send(b'data\n') | ||
|
|
||
| sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock2.connect(("localhost", port)) | ||
| sock2.send(b'data2\n') | ||
| wait_for(lambda: out == [b'data\n', b'data\n', b'data2\n'], 2, | ||
| period=0.01) | ||
| finally: | ||
| s.stop() | ||
| sock.close() | ||
| sock2.close() | ||
|
|
||
|
|
||
| @gen_test(timeout=60) | ||
| def test_tcp_async(): | ||
| port = 9876 | ||
| s = Source.from_tcp(port) | ||
| out = s.sink_to_list() | ||
| s.start() | ||
| yield await_for(lambda: s.server is not None, 2, period=0.02) | ||
|
|
||
| try: | ||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock.connect(("localhost", port)) | ||
| sock.send(b'data\n') | ||
| sock.close() | ||
|
|
||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock.connect(("localhost", port)) | ||
| sock.send(b'data\n') | ||
|
|
||
| sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| sock2.connect(("localhost", port)) | ||
| sock2.send(b'data2\n') | ||
| yield await_for(lambda: out == [b'data\n', b'data\n', b'data2\n'], 2, | ||
| period=0.01) | ||
| finally: | ||
| s.stop() | ||
| sock.close() | ||
| sock2.close() | ||
|
|
||
|
|
||
| def test_http(): | ||
| requests = pytest.importorskip('requests') | ||
| port = 9875 | ||
| s = Source.from_http_server(port) | ||
| out = s.sink_to_list() | ||
| s.start() | ||
| wait_for(lambda: s.server is not None, 2, period=0.02) | ||
|
|
||
| r = requests.post('http://localhost:%i/' % port, data=b'data') | ||
| wait_for(lambda: out == [b'data'], 2, period=0.01) | ||
| assert r.ok | ||
|
|
||
| r = requests.post('http://localhost:%i/other' % port, data=b'data2') | ||
| wait_for(lambda: out == [b'data', b'data2'], 2, period=0.01) | ||
| assert r.ok | ||
|
|
||
| s.stop() | ||
|
|
||
| with pytest.raises(requests.exceptions.RequestException): | ||
| requests.post('http://localhost:%i/other' % port, data=b'data2') |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In one case you use
tcp_kwargsand in the otherserver_kwargs. Maybe useserver_kwargsin both places for consistency of the API?