Skip to content

Commit 1bf04db

Browse files
authored
Python finalize implementation (#2159)
* Python finalize implementation
1 parent 9e5b285 commit 1bf04db

8 files changed

+271
-10
lines changed

src/mediapipe_internal/pythonnoderesource.cpp

+35-9
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,40 @@ namespace ovms {
3636
PythonNodeResource::PythonNodeResource(PythonBackend* pythonBackend) {
3737
this->nodeResourceObject = nullptr;
3838
this->pythonBackend = pythonBackend;
39+
this->pythonNodeFilePath = "";
3940
}
4041

41-
Status PythonNodeResource::createPythonNodeResource(std::shared_ptr<PythonNodeResource>& nodeResource, const google::protobuf::Any& nodeOptions, PythonBackend* pythonBackend) {
42-
mediapipe::PythonExecutorCalculatorOptions options;
43-
nodeOptions.UnpackTo(&options);
44-
if (!std::filesystem::exists(options.handler_path())) {
45-
SPDLOG_LOGGER_DEBUG(modelmanager_logger, "Python node file: {} does not exist. ", options.handler_path());
42+
void PythonNodeResource::finalize() {
43+
if (this->nodeResourceObject) {
44+
py::gil_scoped_acquire acquire;
45+
try {
46+
if (!py::hasattr(*nodeResourceObject.get(), "finalize")) {
47+
SPDLOG_DEBUG("Python node resource does not have a finalize method. Python node path {} ", this->pythonNodeFilePath);
48+
return;
49+
}
50+
51+
nodeResourceObject.get()->attr("finalize")();
52+
} catch (const pybind11::error_already_set& e) {
53+
SPDLOG_ERROR("Failed to process python node finalize method. {} Python node path {} ", e.what(), this->pythonNodeFilePath);
54+
return;
55+
} catch (...) {
56+
SPDLOG_ERROR("Failed to process python node finalize method. Python node path {} ", this->pythonNodeFilePath);
57+
return;
58+
}
59+
} else {
60+
SPDLOG_ERROR("nodeResourceObject is not initialized. Python node path {} ", this->pythonNodeFilePath);
61+
throw std::exception();
62+
}
63+
}
64+
65+
Status PythonNodeResource::createPythonNodeResource(std::shared_ptr<PythonNodeResource>& nodeResource, const google::protobuf::Any& nodeConfig, PythonBackend* pythonBackend) {
66+
mediapipe::PythonExecutorCalculatorOptions nodeOptions;
67+
nodeConfig.UnpackTo(&nodeOptions);
68+
if (!std::filesystem::exists(nodeOptions.handler_path())) {
69+
SPDLOG_LOGGER_DEBUG(modelmanager_logger, "Python node file: {} does not exist. ", nodeOptions.handler_path());
4670
return StatusCode::PYTHON_NODE_FILE_DOES_NOT_EXIST;
4771
}
48-
auto fsHandlerPath = std::filesystem::path(options.handler_path());
72+
auto fsHandlerPath = std::filesystem::path(nodeOptions.handler_path());
4973
fsHandlerPath.replace_extension();
5074

5175
std::string parentPath = fsHandlerPath.parent_path();
@@ -62,17 +86,18 @@ Status PythonNodeResource::createPythonNodeResource(std::shared_ptr<PythonNodeRe
6286
py::bool_ success = pythonModel.attr("initialize")(kwargsParam);
6387

6488
if (!success) {
65-
SPDLOG_ERROR("Python node initialize script call returned false for: {}", options.handler_path());
89+
SPDLOG_ERROR("Python node initialize script call returned false for: {}", nodeOptions.handler_path());
6690
return StatusCode::PYTHON_NODE_FILE_STATE_INITIALIZATION_FAILED;
6791
}
6892

6993
nodeResource = std::make_shared<PythonNodeResource>(pythonBackend);
7094
nodeResource->nodeResourceObject = std::make_unique<py::object>(pythonModel);
95+
nodeResource->pythonNodeFilePath = nodeOptions.handler_path();
7196
} catch (const pybind11::error_already_set& e) {
72-
SPDLOG_ERROR("Failed to process python node file {} : {}", options.handler_path(), e.what());
97+
SPDLOG_ERROR("Failed to process python node file {} : {}", nodeOptions.handler_path(), e.what());
7398
return StatusCode::PYTHON_NODE_FILE_STATE_INITIALIZATION_FAILED;
7499
} catch (...) {
75-
SPDLOG_ERROR("Failed to process python node file {}", options.handler_path());
100+
SPDLOG_ERROR("Failed to process python node file {}", nodeOptions.handler_path());
76101
return StatusCode::PYTHON_NODE_FILE_STATE_INITIALIZATION_FAILED;
77102
}
78103

@@ -81,6 +106,7 @@ Status PythonNodeResource::createPythonNodeResource(std::shared_ptr<PythonNodeRe
81106

82107
PythonNodeResource::~PythonNodeResource() {
83108
SPDLOG_DEBUG("Calling Python node resource destructor");
109+
this->finalize();
84110
py::gil_scoped_acquire acquire;
85111
this->nodeResourceObject.reset();
86112
}

src/mediapipe_internal/pythonnoderesource.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ struct PythonNodeResource {
3838
#if (PYTHON_DISABLE == 0)
3939
std::unique_ptr<py::object> nodeResourceObject;
4040
PythonBackend* pythonBackend;
41+
std::string pythonNodeFilePath;
4142

4243
PythonNodeResource(PythonBackend* pythonBackend);
4344
~PythonNodeResource();
4445
static Status createPythonNodeResource(std::shared_ptr<PythonNodeResource>& nodeResource, const google::protobuf::Any& nodeOptions, PythonBackend* pythonBackend);
46+
void finalize();
4547
#endif
4648
};
4749

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#*****************************************************************************
2+
# Copyright 2023 Intel Corporation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#*****************************************************************************
16+
17+
class OvmsPythonModel:
18+
def initialize(self, kwargs: dict):
19+
return True
20+
21+
def execute(self, inputs: list, kwargs: dict) -> list:
22+
return None
23+
24+
def finalize(self):
25+
return NotExistingException()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#*****************************************************************************
2+
# Copyright 2023 Intel Corporation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#*****************************************************************************
16+
17+
class OvmsPythonModel:
18+
def initialize(self, kwargs: dict):
19+
return True
20+
21+
def execute(self, inputs: list, kwargs: dict) -> list:
22+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#*****************************************************************************
2+
# Copyright 2023 Intel Corporation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#*****************************************************************************
16+
17+
class OvmsPythonModel:
18+
def initialize(self, kwargs: dict):
19+
return True
20+
21+
def execute(self, inputs: list, kwargs: dict) -> list:
22+
return None
23+
24+
def finalize(self):
25+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#*****************************************************************************
2+
# Copyright 2023 Intel Corporation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#*****************************************************************************
16+
17+
import os
18+
19+
class OvmsPythonModel:
20+
def initialize(self, kwargs: dict):
21+
with open("/tmp/pythonNodeTestRemoveFile.txt", 'wt') as file:
22+
file.write("1111")
23+
return True
24+
25+
def execute(self, inputs: list, kwargs: dict) -> list:
26+
return None
27+
28+
def finalize(self):
29+
os.remove("/tmp/pythonNodeTestRemoveFile.txt")
30+
return True

src/test/mediapipe/python/two_python_outputs_graph.pbtxt

+12
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,16 @@ node {
4040
handler_path: "/ovms/src/test/mediapipe/python/scripts/symmetric_increment.py"
4141
}
4242
}
43+
}
44+
node {
45+
name: "pythonNode3"
46+
calculator: "PythonExecutorCalculator"
47+
input_side_packet: "PYTHON_NODE_RESOURCES:py"
48+
input_stream: "INPUT:in"
49+
output_stream: "OUTPUT:out3"
50+
node_options: {
51+
[type.googleapis.com / mediapipe.PythonExecutorCalculatorOptions]: {
52+
handler_path: "/ovms/src/test/mediapipe/python/scripts/good_finalize_remove_file.py"
53+
}
54+
}
4355
}

src/test/pythonnode_test.cpp

+120-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ It's launching along with the server and even most tests will not use the server
7272
std::unique_ptr<std::thread> serverThread;
7373

7474
class PythonFlowTest : public ::testing::TestWithParam<std::pair<std::string, std::string>> {
75-
protected:
75+
public:
7676
static void SetUpTestSuite() {
7777
std::string configPath = "/ovms/src/test/mediapipe/python/mediapipe_add_python_node.json";
7878
ovms::Server::instance().setShutdownRequest(0);
@@ -97,6 +97,8 @@ class PythonFlowTest : public ::testing::TestWithParam<std::pair<std::string, st
9797
ovms::Server::instance().setShutdownRequest(1);
9898
serverThread->join();
9999
ovms::Server::instance().setShutdownRequest(0);
100+
std::string path = std::string("/tmp/pythonNodeTestRemoveFile.txt");
101+
ASSERT_TRUE(!std::filesystem::exists(path));
100102
}
101103
};
102104

@@ -110,6 +112,16 @@ TEST_F(PythonFlowTest, InitializationPass) {
110112
EXPECT_TRUE(graphDefinition->getStatus().isAvailable());
111113
}
112114

115+
TEST_F(PythonFlowTest, FinalizationPass) {
116+
ModelManager* manager;
117+
std::string path = std::string("/tmp/pythonNodeTestRemoveFile.txt");
118+
manager = &(dynamic_cast<const ovms::ServableManagerModule*>(ovms::Server::instance().getModule(SERVABLE_MANAGER_MODULE_NAME))->getServableManager());
119+
auto graphDefinition = manager->getMediapipeFactory().findDefinitionByName("mediapipePythonBackend");
120+
ASSERT_NE(graphDefinition, nullptr);
121+
EXPECT_TRUE(graphDefinition->getStatus().isAvailable());
122+
ASSERT_TRUE(std::filesystem::exists(path));
123+
}
124+
113125
class DummyMediapipeGraphDefinition : public MediapipeGraphDefinition {
114126
public:
115127
std::string inputConfig;
@@ -844,3 +856,110 @@ TEST_F(PythonFlowTest, PythonCalculatorTestSingleInSingleOutMultiRunWithErrors)
844856
- bad input stream element (py::object that is not pyovms.Tensor)
845857
- bad output stream element (py::object that is not pyovms.Tensor)
846858
*/
859+
860+
TEST_F(PythonFlowTest, FinalizePassTest) {
861+
const std::string pbTxt{R"(
862+
input_stream: "in"
863+
output_stream: "out"
864+
node {
865+
name: "pythonNode2"
866+
calculator: "PythonBackendCalculator"
867+
input_side_packet: "PYOBJECT:pyobject"
868+
input_stream: "in"
869+
output_stream: "out2"
870+
node_options: {
871+
[type.googleapis.com / mediapipe.PythonExecutorCalculatorOptions]: {
872+
handler_path: "/ovms/src/test/mediapipe/python/scripts/good_finalize_pass.py"
873+
}
874+
}
875+
}
876+
)"};
877+
::mediapipe::CalculatorGraphConfig config;
878+
ASSERT_TRUE(::google::protobuf::TextFormat::ParseFromString(pbTxt, &config));
879+
880+
std::shared_ptr<PythonNodeResource> nodeResource = nullptr;
881+
ASSERT_EQ(PythonNodeResource::createPythonNodeResource(nodeResource, config.node(0).node_options(0), getPythonBackend()), StatusCode::OK);
882+
nodeResource->finalize();
883+
}
884+
885+
TEST_F(PythonFlowTest, FinalizeMissingPassTest) {
886+
const std::string pbTxt{R"(
887+
input_stream: "in"
888+
output_stream: "out"
889+
node {
890+
name: "pythonNode2"
891+
calculator: "PythonBackendCalculator"
892+
input_side_packet: "PYOBJECT:pyobject"
893+
input_stream: "in"
894+
output_stream: "out2"
895+
node_options: {
896+
[type.googleapis.com / mediapipe.PythonExecutorCalculatorOptions]: {
897+
handler_path: "/ovms/src/test/mediapipe/python/scripts/good_finalize_pass.py"
898+
}
899+
}
900+
}
901+
)"};
902+
::mediapipe::CalculatorGraphConfig config;
903+
ASSERT_TRUE(::google::protobuf::TextFormat::ParseFromString(pbTxt, &config));
904+
905+
std::shared_ptr<PythonNodeResource> nodeResource = nullptr;
906+
ASSERT_EQ(PythonNodeResource::createPythonNodeResource(nodeResource, config.node(0).node_options(0), getPythonBackend()), StatusCode::OK);
907+
nodeResource->finalize();
908+
}
909+
910+
TEST_F(PythonFlowTest, FinalizeDestructorRemoveFileTest) {
911+
const std::string pbTxt{R"(
912+
input_stream: "in"
913+
output_stream: "out"
914+
node {
915+
name: "pythonNode2"
916+
calculator: "PythonBackendCalculator"
917+
input_side_packet: "PYOBJECT:pyobject"
918+
input_stream: "in"
919+
output_stream: "out2"
920+
node_options: {
921+
[type.googleapis.com / mediapipe.PythonExecutorCalculatorOptions]: {
922+
handler_path: "/ovms/src/test/mediapipe/python/scripts/good_finalize_remove_file.py"
923+
}
924+
}
925+
}
926+
)"};
927+
::mediapipe::CalculatorGraphConfig config;
928+
ASSERT_TRUE(::google::protobuf::TextFormat::ParseFromString(pbTxt, &config));
929+
930+
std::string path = std::string("/tmp/pythonNodeTestRemoveFile.txt");
931+
{
932+
std::shared_ptr<PythonNodeResource> nodeResouce = nullptr;
933+
ASSERT_EQ(PythonNodeResource::createPythonNodeResource(nodeResouce, config.node(0).node_options(0), getPythonBackend()), StatusCode::OK);
934+
935+
ASSERT_TRUE(std::filesystem::exists(path));
936+
// nodeResource destructor calls finalize and removes the file
937+
}
938+
939+
ASSERT_TRUE(!std::filesystem::exists(path));
940+
}
941+
942+
TEST_F(PythonFlowTest, FinalizeException) {
943+
const std::string pbTxt{R"(
944+
input_stream: "in"
945+
output_stream: "out"
946+
node {
947+
name: "pythonNode2"
948+
calculator: "PythonBackendCalculator"
949+
input_side_packet: "PYOBJECT:pyobject"
950+
input_stream: "in"
951+
output_stream: "out2"
952+
node_options: {
953+
[type.googleapis.com / mediapipe.PythonExecutorCalculatorOptions]: {
954+
handler_path: "/ovms/src/test/mediapipe/python/scripts/bad_finalize_exception.py"
955+
}
956+
}
957+
}
958+
)"};
959+
::mediapipe::CalculatorGraphConfig config;
960+
ASSERT_TRUE(::google::protobuf::TextFormat::ParseFromString(pbTxt, &config));
961+
962+
std::shared_ptr<PythonNodeResource> nodeResource = nullptr;
963+
ASSERT_EQ(PythonNodeResource::createPythonNodeResource(nodeResource, config.node(0).node_options(0), getPythonBackend()), StatusCode::OK);
964+
nodeResource->finalize();
965+
}

0 commit comments

Comments
 (0)