Browse Source

[2003] TCP send continuation first part: record the context in the server.

also added error case handling.
JINMEI Tatuya 13 years ago
parent
commit
27e2490c86
2 changed files with 77 additions and 8 deletions
  1. 12 5
      src/bin/ddns/ddns.py.in
  2. 65 3
      src/bin/ddns/tests/ddns_test.py

+ 12 - 5
src/bin/ddns/ddns.py.in

@@ -191,12 +191,14 @@ class DDNSServer:
         self.__request_msg = Message(Message.PARSE)
         self.__response_renderer = MessageRenderer()
 
-        # The following attribute(s) are essentially private and constant,
-        # but defined as "protected" so that test code can customize them.
-        # They should not be overridden for any other purposes.
+        # The following attribute(s) are essentially private, but defined as
+        # "protected" so that test code can customize/inspect them.
+        # They should not be overridden/referenced for any other purposes.
         #
         # DDNS Protocol handling class.
         self._UpdateSessionClass = isc.ddns.session.UpdateSession
+        # Outstanding TCP context: fileno=>context_obj
+        self._tcp_ctxs = {}
 
     class InternalError(Exception):
         '''Exception for internal errors in an update session.
@@ -405,8 +407,13 @@ class DDNSServer:
                 sock.sendto(data, dest)
             else:
                 tcp_ctx = DNSTCPContext(sock)
-                tcp_ctx.send(data)
-                tcp_ctx.close()
+                send_result = tcp_ctx.send(data)
+                if send_result == DNSTCPContext.SENDING:
+                    self._tcp_ctxs[sock.fileno] = tcp_ctx
+                elif send_result == DNSTCPContext.CLOSED:
+                    raise socket.error("socket error in TCP send")
+                else:
+                    tcp_ctx.close()
         except socket.error as ex:
             logger.error(DDNS_RESPONSE_SOCKET_ERROR, ClientFormatter(dest), ex)
             return False

+ 65 - 3
src/bin/ddns/tests/ddns_test.py

@@ -22,6 +22,7 @@ import isc.util.cio.socketsession
 from isc.cc.session import SessionTimeout, SessionError, ProtocolError
 from isc.datasrc import DataSourceClient
 from isc.config.ccsession import create_answer
+from isc.server_common.dns_tcp import DNSTCPContext
 import ddns
 import errno
 import os
@@ -65,9 +66,11 @@ class FakeSocket:
         self._sent_data = None
         self._sent_addr = None
         self._close_called = 0  # number of calls to close()
+        self.__send_cc = 0      # waterline of the send buffer (can be reset)
         # customizable by tests; if set to True, sendto() will throw after
         # recording the parameters.
         self._raise_on_send = False
+        self._send_buflen = None # imaginary send buffer for partial send
     def fileno(self):
         return self.__fileno
     def getpeername(self):
@@ -83,11 +86,25 @@ class FakeSocket:
         if self._raise_on_send:
             raise socket.error('test socket failure')
     def send(self, data):
+        if self._raise_on_send:
+            raise socket.error(errno.EPIPE, 'faked connection disruption')
+        elif self._send_buflen is None:
+            available_space = len(data)
+        else:
+            available_space = self._send_buflen - self.__send_cc
+        if available_space == 0:
+            # if there's no space, (assuming it's nonblocking mode) raise
+            # EAGAIN.
+            raise socket.error(errno.EAGAIN,
+                               "Resource temporarily unavailable")
+        # determine the sendable part of the data, record it, update "buffer".
+        cc = min(available_space, len(data))
         if self._sent_data is None:
-            self._sent_data = data
+            self._sent_data = data[:cc]
         else:
-            self._sent_data += data
-        return len(data)
+            self._sent_data += data[:cc]
+        self.__send_cc += cc
+        return cc
     def setblocking(self, on):
         # We only need a faked NO-OP implementation.
         pass
@@ -97,6 +114,10 @@ class FakeSocket:
         '''Clear internal instrumental data.'''
         self._sent_data = None
         self._sent_addr = None
+    def make_send_ready(self):
+        # pretend that the accrued data has been cleared, making room in
+        # the send buffer.
+        self.__send_cc = 0
 
 class FakeSessionReceiver:
     """
@@ -755,6 +776,47 @@ class TestDDNSSession(unittest.TestCase):
         # In the current implementation, the socket should be closed
         # immedidately after a successful send.
         self.assertEqual(1, s._close_called)
+        # TCP context shouldn't be held in the server.
+        self.assertEqual(0, len(self.server._tcp_ctxs))
+
+    def test_tcp_request_incomplete(self):
+        # set the size of the socket "send buffer" to a small value, which
+        # should cause partial send.
+        s = self.__sock
+        s.proto = socket.IPPROTO_TCP
+        s._send_buflen = 7
+        # before this request there should be no outstanding TCP context.
+        self.assertEqual(0, len(self.server._tcp_ctxs))
+        self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
+                                                    TEST_CLIENT6,
+                                                    create_msg())))
+        # Only the part of data that fit the send buffer should be transmitted.
+        self.assertEqual(s._send_buflen, len(s._sent_data))
+        # the socket is not yet closed.
+        self.assertEqual(0, s._close_called)
+        # and a new context is stored in the server.
+        self.assertEqual(1, len(self.server._tcp_ctxs))
+
+        # clear the "send buffer" of the fake socket, and continue the send
+        # by hand.  The next attempt should complete the send, and the combined
+        # data should be the expected response.
+        s.make_send_ready()
+        self.assertEqual(DNSTCPContext.SEND_DONE,
+                         self.server._tcp_ctxs[s.fileno].send_ready())
+        self.server._tcp_ctxs[s.fileno].close() # explicit close per convention
+        self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
+
+    def test_tcp_request_error(self):
+        # initial send() on the TCP socket will fail.  The request handling
+        # will be considered failure.
+        s = self.__sock
+        s.proto = socket.IPPROTO_TCP
+        s._raise_on_send = True
+        self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
+                                                     TEST_CLIENT6,
+                                                     create_msg())))
+        # the socket should have been closed.
+        self.assertEqual(1, s._close_called)
 
     def test_request_message(self):
         '''Test if the request message stores RRs separately.'''