From d58b27437b4204450ae8b5ab15cdddca2c7cb3f4 Mon Sep 17 00:00:00 2001 From: "leo.gao@vesoft.com" <101387006+javaGitHub2022@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:08:16 +0800 Subject: [PATCH] add version selection (#305) * add version selection * add version whitelist * add version whitelist * format * format code * format code * update test_ssl_connection * add handshakekey to sessionPool * add import * update test sessionpool * format code * add test code * add condition for time --------- Co-authored-by: Anqi --- example/SessinPoolExample.py | 4 +- nebula3/Config.py | 3 ++ nebula3/gclient/net/Connection.py | 25 ++++++--- nebula3/gclient/net/ConnectionPool.py | 49 ++++++++++------- nebula3/gclient/net/SessionPool.py | 76 +++++++++++++++------------ tests/test_connection.py | 26 +++++++-- tests/test_session_pool.py | 38 +++++++++++--- tests/test_ssl_connection.py | 33 +++++++++--- 8 files changed, 175 insertions(+), 79 deletions(-) diff --git a/example/SessinPoolExample.py b/example/SessinPoolExample.py index 908af36f..c908a893 100644 --- a/example/SessinPoolExample.py +++ b/example/SessinPoolExample.py @@ -10,15 +10,16 @@ from FormatResp import print_resp + from nebula3.common.ttypes import ErrorCode from nebula3.Config import SessionPoolConfig from nebula3.gclient.net import Connection from nebula3.gclient.net.SessionPool import SessionPool + if __name__ == "__main__": ip = "127.0.0.1" port = 9669 - try: config = SessionPoolConfig() @@ -36,6 +37,7 @@ time.sleep(10) # init session pool + session_pool = SessionPool("root", "nebula", "session_pool_test", [(ip, port)]) assert session_pool.init(config) diff --git a/nebula3/Config.py b/nebula3/Config.py index 55e9d09a..5aa8a515 100644 --- a/nebula3/Config.py +++ b/nebula3/Config.py @@ -25,6 +25,8 @@ class Config(object): # the interval to check idle time connection, unit second, -1 means no check interval_check = -1 + handshakeKey = None + class SSL_config(object): """configs used to Initialize a TSSLSocket. @@ -89,3 +91,4 @@ class SessionPoolConfig(object): max_size = 30 min_size = 1 interval_check = -1 + handshakeKey = None diff --git a/nebula3/gclient/net/Connection.py b/nebula3/gclient/net/Connection.py index fa68cc4a..41215aad 100644 --- a/nebula3/gclient/net/Connection.py +++ b/nebula3/gclient/net/Connection.py @@ -40,23 +40,26 @@ def __init__(self): self._port = None self._timeout = 0 self._ssl_conf = None + self.handshakeKey = None - def open(self, ip, port, timeout): + def open(self, ip, port, timeout, handshakeKey=None): """open the connection :param ip: the server ip :param port: the server port :param timeout: the timeout for connect and execute + :param handshakeKey: the client version :return: void """ - self.open_SSL(ip, port, timeout, None) + self.open_SSL(ip, port, timeout, handshakeKey, None) - def open_SSL(self, ip, port, timeout, ssl_config=None): + def open_SSL(self, ip, port, timeout, handshakeKey=None, ssl_config=None): """open the SSL connection :param ip: the server ip :param port: the server port :param timeout: the timeout for connect and execute + :param handshakeKey: the client version :ssl_config: configs for SSL :return: void """ @@ -64,6 +67,7 @@ def open_SSL(self, ip, port, timeout, ssl_config=None): self._port = port self._timeout = timeout self._ssl_conf = ssl_config + self.handshakeKey = handshakeKey try: if ssl_config is not None: s = TSSLSocket.TSSLSocket( @@ -89,7 +93,10 @@ def open_SSL(self, ip, port, timeout, ssl_config=None): header_transport.open() self._connection = GraphService.Client(protocol) - resp = self._connection.verifyClientVersion(VerifyClientVersionReq()) + verifyClientVersionReq = VerifyClientVersionReq() + if handshakeKey is not None: + verifyClientVersionReq.version = handshakeKey + resp = self._connection.verifyClientVersion(verifyClientVersionReq) if resp.error_code != ErrorCode.SUCCEEDED: self._connection._iprot.trans.close() raise ClientServerIncompatibleException(resp.error_msg) @@ -103,9 +110,11 @@ def _reopen(self): """ self.close() if self._ssl_conf is not None: - self.open_SSL(self._ip, self._port, self._timeout, self._ssl_conf) + self.open_SSL( + self._ip, self._port, self._timeout, self.handshakeKey, self._ssl_conf + ) else: - self.open(self._ip, self._port, self._timeout) + self.open(self._ip, self._port, self._timeout, self.handshakeKey) def authenticate(self, user_name, password): """authenticate to graphd @@ -216,7 +225,7 @@ def close(self): self._connection._iprot.trans.close() except Exception as e: logger.error( - 'Close connection to {}:{} failed:{}'.format(self._ip, self._port, e) + "Close connection to {}:{} failed:{}".format(self._ip, self._port, e) ) def ping(self): @@ -224,7 +233,7 @@ def ping(self): :return: True or False """ try: - resp = self._connection.execute(0, 'YIELD 1;') + resp = self._connection.execute(0, "YIELD 1;") return True except Exception: return False diff --git a/nebula3/gclient/net/ConnectionPool.py b/nebula3/gclient/net/ConnectionPool.py index 5617f270..e7fd010f 100644 --- a/nebula3/gclient/net/ConnectionPool.py +++ b/nebula3/gclient/net/ConnectionPool.py @@ -49,8 +49,8 @@ def init(self, addresses, configs, ssl_conf=None): :return: if all addresses are ok, return True else return False. """ if self._close: - logger.error('The pool has init or closed.') - raise RuntimeError('The pool has init or closed.') + logger.error("The pool has init or closed.") + raise RuntimeError("The pool has init or closed.") self._configs = configs self._ssl_configs = ssl_conf for address in addresses: @@ -73,7 +73,7 @@ def init(self, addresses, configs, ssl_conf=None): ok_num = self.get_ok_servers_num() if ok_num < len(self._addresses): raise RuntimeError( - 'The services status exception: {}'.format(self._get_services_status()) + "The services status exception: {}".format(self._get_services_status()) ) conns_per_address = int(self._configs.min_connection_pool_size / ok_num) @@ -82,7 +82,11 @@ def init(self, addresses, configs, ssl_conf=None): for i in range(0, conns_per_address): connection = Connection() connection.open_SSL( - addr[0], addr[1], self._configs.timeout, self._ssl_configs + addr[0], + addr[1], + self._configs.timeout, + configs.handshakeKey, + self._ssl_configs, ) self._connections[addr].append(connection) return True @@ -135,13 +139,13 @@ def get_connection(self): """ with self._lock: if self._close: - logger.error('The pool is closed') + logger.error("The pool is closed") raise NotValidConnectionException() try: ok_num = self.get_ok_servers_num() if ok_num == 0: - logger.error('No available server') + logger.error("No available server") return None max_con_per_address = int( self._configs.max_connection_pool_size / ok_num @@ -159,7 +163,7 @@ def get_connection(self): # ping to check the connection is valid if connection.ping(): connection.is_used = True - logger.info('Get connection to {}'.format(addr)) + logger.info("Get connection to {}".format(addr)) return connection else: invalid_connections.append(connection) @@ -180,11 +184,12 @@ def get_connection(self): addr[0], addr[1], self._configs.timeout, + self._configs.handshakeKey, self._ssl_configs, ) connection.is_used = True self._connections[addr].append(connection) - logger.info('Get connection to {}'.format(addr)) + logger.info("Get connection to {}".format(addr)) return connection else: for connection in list(self._connections[addr]): @@ -192,10 +197,10 @@ def get_connection(self): self._connections[addr].remove(connection) try_count = try_count + 1 - logger.error('No available connection') + logger.error("No available connection") return None except Exception as ex: - logger.error('Get connection failed: {}'.format(ex)) + logger.error("Get connection failed: {}".format(ex)) return None def ping(self, address): @@ -206,12 +211,18 @@ def ping(self, address): """ try: conn = Connection() - conn.open_SSL(address[0], address[1], 1000, self._ssl_configs) + conn.open_SSL( + address[0], + address[1], + 1000, + self._configs.handshakeKey, + self._ssl_configs, + ) conn.close() return True except Exception as ex: logger.warning( - 'Connect {}:{} failed: {}'.format(address[0], address[1], ex) + "Connect {}:{} failed: {}".format(address[0], address[1], ex) ) return False @@ -224,7 +235,7 @@ def close(self): for addr in self._connections.keys(): for connection in self._connections[addr]: if connection.is_used: - logger.warning('Closing a connection that is in use') + logger.warning("Closing a connection that is in use") connection.close() self._close = True @@ -266,11 +277,11 @@ def get_ok_servers_num(self): def _get_services_status(self): msg_list = [] for addr in self._addresses_status.keys(): - status = 'OK' + status = "OK" if self._addresses_status[addr] != self.S_OK: - status = 'BAD' - msg_list.append('[services: {}, status: {}]'.format(addr, status)) - return ', '.join(msg_list) + status = "BAD" + msg_list.append("[services: {}, status: {}]".format(addr, status)) + return ", ".join(msg_list) def update_servers_status(self): """update the servers' status""" @@ -290,7 +301,7 @@ def _remove_idle_unusable_connection(self): if not connection.is_used: if not connection.ping(): logger.debug( - 'Remove the unusable connection to {}'.format( + "Remove the unusable connection to {}".format( connection.get_address() ) ) @@ -301,7 +312,7 @@ def _remove_idle_unusable_connection(self): and connection.idle_time() > self._configs.idle_time ): logger.debug( - 'Remove the idle connection to {}'.format( + "Remove the idle connection to {}".format( connection.get_address() ) ) diff --git a/nebula3/gclient/net/SessionPool.py b/nebula3/gclient/net/SessionPool.py index 658b2599..af744138 100644 --- a/nebula3/gclient/net/SessionPool.py +++ b/nebula3/gclient/net/SessionPool.py @@ -84,12 +84,12 @@ def init(self, configs): try: self._check_configs() except Exception as e: - logger.error('Invalid configs: {}'.format(e)) + logger.error("Invalid configs: {}".format(e)) return False if self._close: - logger.error('The pool has init or closed.') - raise RuntimeError('The pool has init or closed.') + logger.error("The pool has init or closed.") + raise RuntimeError("The pool has init or closed.") self._configs = configs # ping all servers @@ -101,14 +101,14 @@ def init(self, configs): ok_num = self.get_ok_servers_num() if ok_num < len(self._addresses): raise RuntimeError( - 'The services status exception: {}'.format(self._get_services_status()) + "The services status exception: {}".format(self._get_services_status()) ) # iterate all addresses and create sessions to fullfil the min_size for i in range(self._configs.min_size): session = self._new_session() if session is None: - raise RuntimeError('Get session failed') + raise RuntimeError("Get session failed") self._add_session_to_idle(session) return True @@ -122,14 +122,20 @@ def ping(self, address): try: conn = Connection() if self._ssl_configs is None: - conn.open(address[0], address[1], 1000) + conn.open(address[0], address[1], 1000, self._configs.handshakeKey) else: - conn.open_SSL(address[0], address[1], 1000, self._ssl_configs) + conn.open_SSL( + address[0], + address[1], + 1000, + self._configs.handshakeKey, + self._ssl_configs, + ) conn.close() return True except Exception as ex: logger.warning( - 'Connect {}:{} failed: {}'.format(address[0], address[1], ex) + "Connect {}:{} failed: {}".format(address[0], address[1], ex) ) return False @@ -156,7 +162,7 @@ def execute_parameter(self, stmt, params): """ session = self._get_idle_session() if session is None: - raise RuntimeError('Get session failed') + raise RuntimeError("Get session failed") self._add_session_to_active(session) try: @@ -171,7 +177,7 @@ def execute_parameter(self, stmt, params): return resp except Exception as e: - logger.error('Execute failed: {}'.format(e)) + logger.error("Execute failed: {}".format(e)) # remove the session from the pool if it is invalid self._active_sessions.remove(session) raise e @@ -242,7 +248,7 @@ def execute_json(self, stmt): def execute_json_with_parameter(self, stmt, params): session = self._get_idle_session() if session is None: - raise RuntimeError('Get session failed') + raise RuntimeError("Get session failed") self._add_session_to_active(session) try: @@ -258,7 +264,7 @@ def execute_json_with_parameter(self, stmt, params): return resp except Exception as e: - logger.error('Execute failed: {}'.format(e)) + logger.error("Execute failed: {}".format(e)) # remove the session from the pool if it is invalid self._active_sessions.remove(session) raise e @@ -292,11 +298,11 @@ def get_ok_servers_num(self): def _get_services_status(self): msg_list = [] for addr in self._addresses_status.keys(): - status = 'OK' + status = "OK" if self._addresses_status[addr] != self.S_OK: - status = 'BAD' - msg_list.append('[services: {}, status: {}]'.format(addr, status)) - return ', '.join(msg_list) + status = "BAD" + msg_list.append("[services: {}, status: {}]".format(addr, status)) + return ", ".join(msg_list) def update_servers_status(self): """update the servers' status""" @@ -324,7 +330,7 @@ def _get_idle_session(self): return self._new_session() else: raise NoValidSessionException( - 'The total number of sessions reaches the pool max size {}'.format( + "The total number of sessions reaches the pool max size {}".format( self._configs.max_size ) ) @@ -336,7 +342,7 @@ def _new_session(self): :return: Session """ if self._ssl_configs is not None: - raise RuntimeError('SSL is not supported yet') + raise RuntimeError("SSL is not supported yet") self._pos = (self._pos + 1) % len(self._addresses) next_addr_index = self._pos @@ -349,7 +355,7 @@ def _new_session(self): # if the address is bad, skip it if self._addresses_status[addr] == self.S_BAD: - logger.warning('The graph service {} is not available'.format(addr)) + logger.warning("The graph service {} is not available".format(addr)) retries = retries - 1 next_addr_index = (next_addr_index + 1) % len(self._addresses) continue @@ -362,10 +368,10 @@ def _new_session(self): session = Session(connection, auth_result, self, False) # switch to the space specified in the configs - resp = session.execute('USE {}'.format(self._space_name)) + resp = session.execute("USE {}".format(self._space_name)) if not resp.is_succeeded(): raise RuntimeError( - 'Failed to get session, cannot set the session space to {} error: {} {}'.format( + "Failed to get session, cannot set the session space to {} error: {} {}".format( self._space_name, resp.error_code(), resp.error_msg() ) ) @@ -376,7 +382,7 @@ def _new_session(self): "User not exist" ): logger.error( - 'Authentication failed, because of bad credentials, close the pool {}'.format( + "Authentication failed, because of bad credentials, close the pool {}".format( e ) ) @@ -386,7 +392,7 @@ def _new_session(self): raise raise RuntimeError( - 'Failed to get a valid session, no graph service is available' + "Failed to get a valid session, no graph service is available" ) def _return_session(self, session): @@ -428,14 +434,14 @@ def _set_space_to_default(self, session): :return: void """ try: - resp = session.execute('USE {}'.format(self._space_name)) + resp = session.execute("USE {}".format(self._space_name)) if not resp.is_succeeded(): raise RuntimeError( - 'Failed to set the session space to {}'.format(self._space_name) + "Failed to set the session space to {}".format(self._space_name) ) except Exception: logger.warning( - 'Failed to set the session space to {}, the current session has been dropped'.format( + "Failed to set the session space to {}, the current session has been dropped".format( self._space_name ) ) @@ -474,23 +480,23 @@ def _period_detect(self): def _check_configs(self): """validate the configs""" if self._configs.min_size < 0: - raise RuntimeError('The min_size must be greater than 0') + raise RuntimeError("The min_size must be greater than 0") if self._configs.max_size < 0: - raise RuntimeError('The max_size must be greater than 0') + raise RuntimeError("The max_size must be greater than 0") if self._configs.min_size > self._configs.max_size: raise RuntimeError( - 'The min_size must be less than or equal to the max_size' + "The min_size must be less than or equal to the max_size" ) if self._configs.idle_time < 0: - raise RuntimeError('The idle_time must be greater or equal to 0') + raise RuntimeError("The idle_time must be greater or equal to 0") if self._configs.timeout < 0: - raise RuntimeError('The timeout must be greater or equal to 0') + raise RuntimeError("The timeout must be greater or equal to 0") if self._space_name == "": - raise RuntimeError('The space_name must be set') + raise RuntimeError("The space_name must be set") if self._username == "": - raise RuntimeError('The username must be set') + raise RuntimeError("The username must be set") if self._password == "": - raise RuntimeError('The password must be set') + raise RuntimeError("The password must be set") if self._addresses is None or len(self._addresses) == 0: - raise RuntimeError('The addresses must be set') + raise RuntimeError("The addresses must be set") diff --git a/tests/test_connection.py b/tests/test_connection.py index fead1345..2ec28dba 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -7,6 +7,14 @@ import time +import sys +import os + +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.join(current_dir, "..") +sys.path.insert(0, root_dir) + + from unittest import TestCase from nebula3.common import ttypes @@ -15,6 +23,7 @@ AddrIp = ["127.0.0.1", "::1"] port = 9669 +handshakeKey = "3.0.0" class TestConnection(TestCase): @@ -22,18 +31,29 @@ def test_create(self): for ip in AddrIp: try: conn = Connection() - conn.open(ip, port, 1000) + conn.open(ip, port, 1000, handshakeKey) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close() except Exception as ex: assert False, ex + def test_create_connect_not_in_whitelist(self): + for ip in AddrIp: + try: + conn = Connection() + conn.open(ip, port, 1000, "invalid_handshakeKey") + auth_result = conn.authenticate("root", "nebula") + assert auth_result.get_session_id() != 0 + conn.close() + except Exception as ex: + assert True, ex + def test_release(self): for ip in AddrIp: try: conn = Connection() - conn.open(ip, port, 1000) + conn.open(ip, port, 1000, handshakeKey) auth_result = conn.authenticate("root", "nebula") session_id = auth_result.get_session_id() assert session_id != 0 @@ -51,7 +71,7 @@ def test_release(self): def test_close(self): for ip in AddrIp: conn = Connection() - conn.open(ip, port, 1000) + conn.open(ip, port, 1000, handshakeKey) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close() diff --git a/tests/test_session_pool.py b/tests/test_session_pool.py index f508dc3a..57e08439 100644 --- a/tests/test_session_pool.py +++ b/tests/test_session_pool.py @@ -9,6 +9,14 @@ import json import threading import time +import os +import sys + +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.join(current_dir, "..") +sys.path.insert(0, root_dir) + + from unittest import TestCase from nebula3.common.ttypes import ErrorCode @@ -22,12 +30,13 @@ # ports for test test_port = 9669 test_port2 = 9670 +handshakeKey = "3.0.0" def prepare_space(space_name="session_pool_test"): # prepare space conn = Connection() - conn.open("127.0.0.1", test_port, 1000) + conn.open("127.0.0.1", test_port, 1000, handshakeKey) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 resp = conn.execute( @@ -65,6 +74,7 @@ def setup_class(self): self.configs.max_size = 4 self.configs.idle_time = 2000 self.configs.interval_check = 2 + self.configs.handshakeKey = "3.0.0" # prepare space prepare_space("session_pool_test") @@ -74,7 +84,10 @@ def setup_class(self): time.sleep(10) self.session_pool = SessionPool( - "root", "nebula", "session_pool_test", self.addresses + "root", + "nebula", + "session_pool_test", + self.addresses, ) assert self.session_pool.init(self.configs) @@ -85,13 +98,19 @@ def tearDown_Class(self): def test_pool_init(self): # basic session_pool = SessionPool( - "root", "nebula", "session_pool_test", self.addresses + "root", + "nebula", + "session_pool_test", + self.addresses, ) assert session_pool.init(self.configs) # handle wrong service port pool = SessionPool( - "root", "nebula", "session_pool_test", [("127.0.0.1", 3800)] + "root", + "nebula", + "session_pool_test", + [("127.0.0.1", 3800)], ) # wrong port try: pool.init(self.configs) @@ -102,7 +121,10 @@ def test_pool_init(self): # handle invalid hostname try: session_pool = SessionPool( - "root", "nebula", "session_pool_test", [("wrong_host", test_port)] + "root", + "nebula", + "session_pool_test", + [("wrong_host", test_port)], ) session_pool.init(self.configs) assert False @@ -128,7 +150,10 @@ def test_switch_space(self): # This test is used to test if the space bond to session is the same as the space in the session pool config after executing # a query contains `USE ` statement. session_pool = SessionPool( - "root", "nebula", "session_pool_test", self.addresses + "root", + "nebula", + "session_pool_test", + self.addresses, ) configs = SessionPoolConfig() configs.min_size = 1 @@ -155,6 +180,7 @@ def test_session_pool_multi_thread(): configs.max_size = 4 configs.idle_time = 2000 configs.interval_check = 2 + configs.handshakeKey = "3.0.0" session_pool = SessionPool("root", "nebula", "session_pool_test", addresses) assert session_pool.init(configs) diff --git a/tests/test_ssl_connection.py b/tests/test_ssl_connection.py index 73b9dc5f..28c7f56c 100644 --- a/tests/test_ssl_connection.py +++ b/tests/test_ssl_connection.py @@ -11,7 +11,14 @@ from unittest import TestCase import pytest +import sys +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.join(current_dir, "..") +sys.path.insert(0, root_dir) + +from unittest import TestCase +from nebula3.Exception import IOErrorException from nebula3.common import ttypes from nebula3.Config import SSL_config from nebula3.Exception import IOErrorException @@ -36,6 +43,7 @@ host = "127.0.0.1" port = 9669 +handshakeKey = "3.0.0" @pytest.mark.SSL @@ -43,17 +51,16 @@ class TestSSLConnection(TestCase): def test_create(self): try: conn = Connection() - conn.open_SSL(host, port, 1000, ssl_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_config) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close() except Exception as ex: assert False, ex - def test_release(self): try: conn = Connection() - conn.open_SSL(host, port, 1000, ssl_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_config) auth_result = conn.authenticate("root", "nebula") session_id = auth_result.get_session_id() assert session_id != 0 @@ -70,7 +77,7 @@ def test_release(self): def test_close(self): conn = Connection() - conn.open_SSL(host, port, 1000, ssl_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_config) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close() @@ -85,17 +92,29 @@ class TestSSLConnectionSelfSigned(TestCase): def test_create_self_signed(self): try: conn = Connection() - conn.open_SSL(host, port, 1000, ssl_selfs_signed_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_selfs_signed_config) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close() except Exception as ex: assert False, ex + def test_create_self_signed_not_in_whitelist(self): + try: + conn = Connection() + conn.open_SSL( + host, port, 1000, "invalid_handshakeKey", ssl_selfs_signed_config + ) + auth_result = conn.authenticate("root", "nebula") + assert auth_result.get_session_id() != 0 + conn.close() + except Exception as ex: + assert True, ex + def test_release_self_signed(self): try: conn = Connection() - conn.open_SSL(host, port, 1000, ssl_selfs_signed_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_selfs_signed_config) auth_result = conn.authenticate("root", "nebula") session_id = auth_result.get_session_id() assert session_id != 0 @@ -112,7 +131,7 @@ def test_release_self_signed(self): def test_close_self_signed(self): conn = Connection() - conn.open_SSL(host, port, 1000, ssl_selfs_signed_config) + conn.open_SSL(host, port, 1000, handshakeKey, ssl_selfs_signed_config) auth_result = conn.authenticate("root", "nebula") assert auth_result.get_session_id() != 0 conn.close()