-
Notifications
You must be signed in to change notification settings - Fork 96
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
Implement Unix Socket Controller #247
Conversation
Split the BaseThreadedController logic from Controller, and make BaseThreadedController an Abstract Base Class. This is in preparation of implementing the UnixSocketController, which shares lots of logic, but not quite, with Controller.
Also exclude this class from testing if on Windows.
Inspired by aio-libs#244
Too noisy. log.warning is more subtle but still provide ample warning.
So we can ensure that it works. A test case accompanies this change.
Because that is its purpose: To trigger creation of the SMTP server.
* BaseThreadedController is its own entry * Controller reduced to just the difference with BaseThreadedController * UnixSocketController added in same vein
Because pytest's doctest runner can't conditionally skip on blocks.
* Indentation: the ":skipif:" option must be aligned with the doctest * AF_UNIX instead of AF_INET * Grab first response after connecting * Some proper SMTP exchange * Closing code * And replace a blank line with >>>
* Remove unused things * Update Sphinx requirement
I forgot that the "unix_socket" is a different group, and thus have different imports, classes, etc. Was not visible in Windows because it's skipped, but it exploded on Linux So rather than using `ExampleHandler` (which was a lengthy custom class) , we simply use Sink instead.
While we're at it, let's "decorate" that lone controller.stop() line.
Now that we run sphinx-build directly and not through setup.py, let's add the "--color" options to make life more colorful :)
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.
Here are some comments for some possibly interesting points...
pytest -v aiosmtpd/docs | ||
python setup.py build_sphinx | ||
sphinx-build -b doctest -d build/.doctree aiosmtpd/docs build/doctest | ||
sphinx-build -b html -d build/.doctree aiosmtpd/docs build/html |
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.
pytest
doctest can't conditionally skip blocks. Which we need to do on Win32.
So we now use Sphinx' own sphinx.ext.doctest
.
srv_coro: Coroutine = self.loop.create_server( | ||
self._factory_invoker, | ||
host=self.hostname, | ||
port=self.port, | ||
ssl=self.ssl_context, | ||
) |
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.
Shifted to self._create_server
to allow different methods of listening.
aiosmtpd/controller.py
Outdated
if not ready_event.is_set(): | ||
raise TimeoutError("SMTP server failed to start within allotted time") |
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.
Previously we just wait and didn't actually do anything if timeout is reached.
This is now fixed, inspired by #244
aiosmtpd/controller.py
Outdated
class UnixSocketController(BaseThreadedController): # pragma: on-win32 | ||
def __init__( | ||
self, | ||
handler, | ||
unix_socket: Optional[Union[str, Path]], | ||
loop=None, | ||
*, | ||
ready_timeout=1.0, | ||
ssl_context=None, | ||
# SMTP parameters | ||
server_hostname: str = None, | ||
**SMTP_parameters, | ||
): | ||
super().__init__( | ||
handler, | ||
loop, | ||
ready_timeout=ready_timeout, | ||
ssl_context=ssl_context, | ||
server_hostname=server_hostname, | ||
**SMTP_parameters | ||
) | ||
self.unix_socket = str(unix_socket) | ||
|
||
def _create_server(self) -> Coroutine: | ||
return self.loop.create_unix_server( | ||
self._factory_invoker, | ||
path=self.unix_socket, | ||
ssl=self.ssl_context, | ||
) | ||
|
||
def _trigger_server(self): | ||
with ExitStack() as stk: | ||
s: socket = stk.enter_context(socket(AF_UNIX, SOCK_STREAM)) | ||
s.connect(self.unix_socket) | ||
if self.ssl_context: | ||
s = stk.enter_context(self.ssl_context.wrap_socket(s)) | ||
_ = s.recv(1024) |
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.
New class! The gist of Issue #114
It's amazing how simple the diffcode is after the shared code is extracted to BaseThreadedController
@@ -51,21 +47,23 @@ def syspath_insert(pth: Path): | |||
|
|||
# autoprogramm needs Sphinx>=1.2.2 | |||
# :classmethod: needs Sphinx>=2.1 | |||
needs_sphinx = "2.1" | |||
# :noindex: needs Sphinx>=3.2 | |||
needs_sphinx = "3.2" |
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.
RTD uses 3.3.1 so this is still safe.
aiosmtpd/tests/test_server.py
Outdated
def in_win32(): | ||
return platform.system().casefold() == "windows" |
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.
UnixSocketController
is not testable on Windows 32.
aiosmtpd/docs/controller.rst
Outdated
.. doctest:: unix_socket | ||
:skipif: in_win32 |
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.
The :skipif:
there is why I revert from using pytest doctest and change to using sphinx-build -b doctest
instead.
aiosmtpd/tests/test_server.py
Outdated
def test_reuse_loop(self, temp_event_loop): | ||
cont = Controller(Sink(), loop=temp_event_loop) | ||
assert cont.loop is temp_event_loop | ||
try: | ||
cont.start() | ||
assert cont.smtpd.loop is temp_event_loop | ||
finally: | ||
cont.stop() |
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.
Apparently we haven't test the loop
behavior before...
Test it as well.
# addopts = """--doctest-glob="*.rst" --strict-markers -rfEX""" | ||
addopts = """--strict-markers -rfEX""" |
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.
We're now using Sphinx's sphinx.ext.doctest
which is more flexible.
sphinx-build --color -b doctest -d build/.doctree aiosmtpd/docs build/doctest | ||
sphinx-build --color -b html -d build/.doctree aiosmtpd/docs build/html |
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.
This makes things a bit faster, actually, since the doctest builder and the html builder shared the same doctree.
Also, add expected message for TimeoutError
b84fe6c
to
ec8528e
Compare
Codecov Report
@@ Coverage Diff @@
## master #247 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 7 7
Lines 1571 1606 +35
Branches 293 297 +4
=========================================
+ Hits 1571 1606 +35
Continue to review full report at Codecov.
|
Ugh, got hit by "AF_UNIX path too long" issue on MacOS 😑 Same issue as aio-libs/aiohttp#3572 Their fix is a bit too complex for my liking, though ... Let me write a simpler fixture for aiosmtpd 😉 |
We can now skip the assert; useful for exception handling in which stop() needs to be manually invoked, but thread possibly hasn't started yet. Also, added some ref-cleaning to assist gc. Finally, we added some test cases.
By setting its scope to "module", so it will be called -- and cleaned up -- exactly once.
aiosmtpd/controller.py
Outdated
if not self._factory_invoked.wait(self.ready_timeout): | ||
raise TimeoutError("SMTP server not responding within allotted time") | ||
if self._thread_exception is not None: | ||
raise self._thread_exception |
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.
If for some reasons factory()
or _factory_invoker()
take their sweet time in doing their job, we might miss a "late exception" because when main thread progresses to if self._thread_exception
line there, _factory_invoker()
might not have the opportunity yet to set the self._thread_exception
variable.
Ideally, the timeout should be "what's left after waiting for create_server()", but that adds more complexity and I'm reluctant to do that.
def stop(self, no_assert=False): | ||
assert no_assert or self._thread is not None, "SMTP daemon not running" | ||
self.loop.call_soon_threadsafe(self._stop) | ||
self._thread.join() | ||
self._thread = None | ||
if self._thread is not None: | ||
self._thread.join() | ||
self._thread = None |
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.
We now can bypass the assert test. Helpful for external exception handling because at that time, thread is possibly not started yet.
aiosmtpd/tests/test_server.py
Outdated
def factory(self): | ||
time.sleep(self.ready_timeout * 3) | ||
return super().factory() | ||
|
||
def _factory_invoker(self): | ||
time.sleep(self.ready_timeout * 3) | ||
return super()._factory_invoker() |
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.
Despite the HUGE multiplier (total is 6), this does not affect test speed because test speed depends solely on the ready_timeout
parameter.
Due to the urgent bugfix of #248, we'll squamerge this not earlier than Friday (2021-02-19). To give time to marinate this PR after merging the changed master into this PR. |
# Conflicts: # aiosmtpd/__init__.py # aiosmtpd/controller.py # aiosmtpd/docs/NEWS.rst # aiosmtpd/docs/controller.rst # aiosmtpd/tests/test_server.py
# Conflicts: # aiosmtpd/__init__.py # aiosmtpd/docs/controller.rst
d778c05
to
2ef7e38
Compare
# Conflicts: # .github/workflows/unit-testing-and-coverage.yml # aiosmtpd/docs/NEWS.rst # aiosmtpd/docs/conf.py # tox.ini
Artefact due to merging with master. After all, we have changed to using sphinx-build doctest (5156b4d)
In accordance with the Developer's Guidelines: https://github.com/aio-libs/aiosmtpd/wiki/Configuration Also, fix a typo in tox.ini
Cygwin's AF_UNIX is emulated using AF_INET, and there are quirks that will cause problems with how we use AF_UNIX. So we disable the doctest on Cygwin as well, disable the whole Test Class, and exclude the UnixSocketController from coverage.
# Conflicts: # .gitignore # aiosmtpd/docs/NEWS.rst # pyproject.toml # tox.ini
pytest -v aiosmtpd/docs | ||
sphinx-build --color -b html -d build/.doctree aiosmtpd/docs build/html | ||
sphinx-build --color -b man -d build/.doctree aiosmtpd/docs build/man | ||
sphinx-build --color -b doctest -d build/.doctree aiosmtpd/docs build/doctest |
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.
We need selective doctest (UnixSocketController doesn't work on Windows). pytest can't do that, but sphinx-build can.
A side benefit: No longer receive the DeprecationWarning warning :-)
_ = s.recv(1024) | ||
|
||
|
||
class UnixSocketController(BaseThreadedController): # pragma: on-win32 on-cygwin |
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.
This class doesn't work properly on Windows and/or Cygwin.
doctest_global_setup = """ | ||
import sys | ||
in_win32 = sys.platform == "win32" | ||
in_cygwin = sys.platform == "cygwin" | ||
""" |
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.
Need to check for Cygwin as well (UnixSocketController does not work properly on Cygwin)
@pytest.fixture(scope="module") | ||
def safe_socket_dir() -> Generator[Path, None, None]: | ||
# See: | ||
# - https://github.com/aio-libs/aiohttp/issues/3572 | ||
# - https://github.com/aio-libs/aiohttp/pull/3832/files | ||
# - https://unix.stackexchange.com/a/367012/5589 | ||
tmpdir = Path(mkdtemp()).absolute() | ||
assert len(str(tmpdir)) <= 87 # 92 (max on HP-UX) minus 5 (allow 4-char fn) | ||
# | ||
yield tmpdir | ||
# | ||
plist = [p for p in tmpdir.rglob("*")] | ||
for p in reversed(plist): | ||
if p.is_dir(): | ||
p.rmdir() | ||
else: | ||
p.unlink() | ||
tmpdir.rmdir() |
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.
To create socket successfully in MacOS. The GA runner path is too deep on MacOS.
[flake8] | ||
jobs = 1 | ||
max-line-length = 88 | ||
ignore = E123, E133, W503, W504, W293, E203 |
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.
Moved to setup.cfg
. See wiki for reasoning.
As soon as this last commit passes green, I'll squamerge. |
Also use sed instead of grep
What do these changes do?
Add a new controller class,
UnixSocketController
, that listens on a Unix socket file instead of IP:portAre there changes in behavior for the user?
Due to decomposition of
Controller
intoBaseThreadedController
+ (pared down)Controller
, user expectingController
to not have a superclass is in for a surprise.Other than that, there should be no problems.
Related issue number
Closes #114
Checklist
(ALL)
(ALL)
qa,py-nocov
(ALL)
+pypy3-{nocov,cov,diffcov}
(ALL)
+pypy3-{nocov,cov,diffcov}
(ALL)
+pypy3-{nocov,cov,diffcov}
NEWS.rst
file