From b87bafd4e5627f2715a419e2c9fd3be85a91c97e Mon Sep 17 00:00:00 2001 From: Dario Ernst Date: Mon, 19 Oct 2020 18:19:54 +0200 Subject: [PATCH 1/2] Add batching functionality to delete_nodes When `delete_nodes` is given a large amount of nodes, or is instructed to recursively delete a big structure, the connection between client and server can time out. To remedy this, allow the user to supply a `batch` parameter, which controls how many nodes are deleted in one request. closes #1148 --- opcua/client/client.py | 4 ++-- opcua/common/manage_nodes.py | 32 ++++++++++++++++++++++++++------ opcua/server/server.py | 4 ++-- tests/tests_common.py | 24 ++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/opcua/client/client.py b/opcua/client/client.py index 04d1e0bc3..a46de5470 100644 --- a/opcua/client/client.py +++ b/opcua/client/client.py @@ -586,8 +586,8 @@ def get_namespace_index(self, uri): uries = self.get_namespace_array() return uries.index(uri) - def delete_nodes(self, nodes, recursive=False): - return delete_nodes(self.uaclient, nodes, recursive) + def delete_nodes(self, nodes, recursive=False, batch=False): + return delete_nodes(self.uaclient, nodes, recursive, batch=batch) def import_xml(self, path=None, xmlstring=None): """ diff --git a/opcua/common/manage_nodes.py b/opcua/common/manage_nodes.py index 25619a3fc..434af1b3c 100644 --- a/opcua/common/manage_nodes.py +++ b/opcua/common/manage_nodes.py @@ -372,10 +372,13 @@ def _guess_datatype(variant): return ua.NodeId(getattr(ua.ObjectIds, variant.VariantType.name)) -def delete_nodes(server, nodes, recursive=False, delete_target_references=True): +def delete_nodes(server, nodes, recursive=False, delete_target_references=True, batch=False): """ - Delete specified nodes. Optionally delete recursively all nodes with a - downward hierachic references to the node + Delete specified nodes. Optionally recursively delete all nodes with a + downward hierachic references to the node. + `batch` can be False or an integer denoting the maximum amount of items per + request. If an integer is given, batches the discovered nodes into multiple + DeleteNodesRequests. return the list of deleted node and the result """ nodestodelete = [] @@ -386,9 +389,26 @@ def delete_nodes(server, nodes, recursive=False, delete_target_references=True): it.NodeId = mynode.nodeid it.DeleteTargetReferences = delete_target_references nodestodelete.append(it) - params = ua.DeleteNodesParameters() - params.NodesToDelete = nodestodelete - return nodes, server.delete_nodes(params) + + if batch is True: + batch = 100 + + if batch and isinstance(batch, int): + chunks = [ + nodestodelete[i : i + batch] + for i in range(0, len(nodestodelete), batch) + ] + + results = [] + for chunk in chunks: + params = ua.DeleteNodesParameters() + params.NodesToDelete = chunk + results.extend(server.delete_nodes(params)) + return nodes, results + else: + params = ua.DeleteNodesParameters() + params.NodesToDelete = nodestodelete + return nodes, server.delete_nodes(params) def _add_childs(nodes): diff --git a/opcua/server/server.py b/opcua/server/server.py index af69bfd24..18d808fc9 100644 --- a/opcua/server/server.py +++ b/opcua/server/server.py @@ -541,8 +541,8 @@ def export_xml_by_ns(self, path, namespaces=None): nodes = get_nodes_of_namespace(self, namespaces) self.export_xml(nodes, path) - def delete_nodes(self, nodes, recursive=False): - return delete_nodes(self.iserver.isession, nodes, recursive) + def delete_nodes(self, nodes, recursive=False, batch=False): + return delete_nodes(self.iserver.isession, nodes, recursive, batch=batch) def historize_node_data_change(self, node, period=timedelta(days=7), count=0): """ diff --git a/tests/tests_common.py b/tests/tests_common.py index 5def9436e..edab1aa01 100644 --- a/tests/tests_common.py +++ b/tests/tests_common.py @@ -173,6 +173,30 @@ def test_delete_nodes_recursive2(self): with self.assertRaises(ua.UaStatusCodeError): node.get_browse_name() + def test_delete_nodes_recursive_batching(self): + obj = self.opc.get_objects_node() + fold = obj.add_folder(2, "FolderToDeleteRoot") + nfold = fold + mynodes = [] + for i in range(7): + nfold = fold.add_folder(2, "FolderToDeleteRoot") + var = fold.add_variable(2, "VarToDeleteR", 9.1) + var = fold.add_property(2, "ProToDeleteR", 9.1) + prop = fold.add_property(2, "ProToDeleteR", 9.1) + o = fold.add_object(3, "ObjToDeleteR") + o_var = o.add_variable(3, "VarToDeleteRR", 9.2) + o_prop = o.add_property(3, "PropToDeleteRR", 9.2) + mynodes.append(nfold) + mynodes.append(var) + mynodes.append(prop) + mynodes.append(o) + mynodes.append(o_var) + mynodes.append(o_prop) + self.opc.delete_nodes([fold], recursive=True, batch=2) + for node in mynodes: + with self.assertRaises(ua.UaStatusCodeError): + node.get_browse_name() + def test_delete_references(self): newtype = self.opc.get_node(ua.ObjectIds.HierarchicalReferences).add_reference_type(0, "HasSuperSecretVariable") From 13eeb18b7651a50fe40c71fa8e4dadca13a320e8 Mon Sep 17 00:00:00 2001 From: Dario Ernst Date: Mon, 19 Oct 2020 18:22:14 +0200 Subject: [PATCH 2/2] Fix flaky test that failed ~50% of the time --- tests/tests_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_common.py b/tests/tests_common.py index edab1aa01..5404b7eee 100644 --- a/tests/tests_common.py +++ b/tests/tests_common.py @@ -465,7 +465,7 @@ def test_add_find_node_(self): def test_node_path(self): objects = self.opc.get_objects_node() - o = objects.add_object('ns=2;i=105;', '2:NodePathObject') + o = objects.add_object('ns=2;i=1005;', '2:NodePathObject') root = self.opc.get_root_node() o2 = root.get_child(['0:Objects', '2:NodePathObject']) self.assertEqual(o, o2)