Skip to content

Commit c064e59

Browse files
authored
Merge pull request #27 from feedzai/fix-model-with-non-thread-safe-dependencies
Fix load of models that depend on non thread-safe dependencies
2 parents 7f2d92f + 1337706 commit c064e59

10 files changed

Lines changed: 360 additions & 7 deletions

File tree

openml-python-common/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ export PATH=$ANACONDA_PATH/envs/myenv/bin:$PATH
4040
export LD_LIBRARY_PATH=$ANACONDA_PATH/envs/myenv/lib/python3.6/site-packages/jep:$LD_LIBRARY_PATH
4141
export LD_PRELOAD=$ANACONDA_PATH/envs/myenv/lib/libpython3.6m.so
4242
```
43+
44+
7. If you need to share Python modules across sub-interpreters, you will need to create a "python-packages.xml" file where you define the modules to be shared. By default the provider is already sharing the "numpy" and "tensorflow" modules. This is a workaround for the issues with CPython extensions.
45+
- Remember that this file should be added to the classpath of your program.
46+
47+
```
48+
<?xml version="1.0"?>
49+
<python>
50+
<package>my_package_1</package>
51+
<package>my_package_2</package>
52+
</python>
53+
54+
```

openml-python-common/pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@
4242
<groupId>com.feedzai</groupId>
4343
<artifactId>openml-utils</artifactId>
4444
</dependency>
45+
<dependency>
46+
<groupId>com.fasterxml.jackson.core</groupId>
47+
<artifactId>jackson-databind</artifactId>
48+
<scope>provided</scope>
49+
</dependency>
4550
<dependency>
4651
<groupId>com.feedzai</groupId>
4752
<artifactId>openml-utils</artifactId>
4853
<scope>test</scope>
4954
<type>test-jar</type>
5055
</dependency>
51-
<dependency>
52-
<groupId>com.fasterxml.jackson.core</groupId>
53-
<artifactId>jackson-databind</artifactId>
54-
<scope>provided</scope>
55-
</dependency>
5656
</dependencies>
5757
</project>

openml-python-common/src/main/java/com/feedzai/openml/python/jep/instance/JepInstance.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
*/
1616
package com.feedzai.openml.python.jep.instance;
1717

18+
import com.feedzai.openml.python.modules.SharedModulesParser;
19+
import com.google.common.collect.ImmutableSet;
1820
import com.google.common.util.concurrent.Uninterruptibles;
1921
import jep.Jep;
22+
import jep.JepConfig;
2023
import jep.JepException;
2124
import org.slf4j.Logger;
2225
import org.slf4j.LoggerFactory;
2326

27+
import java.util.Set;
2428
import java.util.concurrent.BlockingQueue;
2529
import java.util.concurrent.LinkedBlockingQueue;
2630
import java.util.concurrent.TimeUnit;
@@ -97,8 +101,18 @@ public void stop() {
97101
*/
98102
@Override
99103
public void run() {
100-
101-
try (final Jep jep = new Jep(false)) {
104+
final Set<String> sharedModules = ImmutableSet.<String>builder()
105+
.add("tensorflow")
106+
.add("numpy")
107+
.addAll(new SharedModulesParser().getSharedModules())
108+
.build();
109+
logger.debug("Python modules to be shared: {}", String.join(",", sharedModules.toString()));
110+
111+
final JepConfig jepConfig = new JepConfig()
112+
.addSharedModules(sharedModules.toArray(new String[0]))
113+
.setInteractive(false);
114+
115+
try (final Jep jep = new Jep(jepConfig)) {
102116
while (this.running) {
103117
this.evaluationQueue.take().evaluate(jep);
104118
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2018 Feedzai
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+
package com.feedzai.openml.python.modules;
18+
19+
import com.google.common.collect.ImmutableSet;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.w3c.dom.Document;
23+
import org.w3c.dom.NodeList;
24+
25+
import javax.xml.parsers.DocumentBuilder;
26+
import javax.xml.parsers.DocumentBuilderFactory;
27+
import java.io.InputStream;
28+
import java.util.Set;
29+
30+
/**
31+
* Class responsible for parsing a XML file in order to retrieve the name of the Python modules to be shared across
32+
* sub-interpreters.
33+
*
34+
* @author Paulo Pereira (paulo.pereira@feedzai.com)
35+
* @since 0.1.5
36+
*/
37+
public class SharedModulesParser {
38+
39+
/**
40+
* Logger.
41+
*/
42+
private static final Logger logger = LoggerFactory.getLogger(SharedModulesParser.class);
43+
44+
/**
45+
* Default value for {@link #xmlFile}.
46+
*/
47+
private static final String DEFAULT_XML_FILE = "python-packages.xml";
48+
49+
/**
50+
* The filename of a XML file with the name of the Python modules to be shared across sub-interpreters.
51+
*/
52+
private final String xmlFile;
53+
54+
/**
55+
* Constructor.
56+
*
57+
* @param xmlFileName The name of a XML file.
58+
*/
59+
public SharedModulesParser(final String xmlFileName) {
60+
this.xmlFile = xmlFileName;
61+
}
62+
63+
/**
64+
* Constructor.
65+
*/
66+
public SharedModulesParser() {
67+
this(DEFAULT_XML_FILE);
68+
}
69+
70+
/**
71+
* Gets the {@link InputStream} of {@link #xmlFile} that exists in the current classpath.
72+
*
73+
* @return The {@link InputStream} of {@link #xmlFile}.
74+
*/
75+
private InputStream getXMLInputStream() {
76+
final ClassLoader classLoader = getClass().getClassLoader();
77+
return classLoader.getResourceAsStream(this.xmlFile);
78+
}
79+
80+
/**
81+
* Parses the {@link #xmlFile} to retrieve the name of the Python modules to be shared across sub-interpreters.
82+
*
83+
* @return A {@link Set} with the name of the Python modules to be shared across sub-interpreters.
84+
* @throws Exception If there is an error while parsing {@link #xmlFile}.
85+
*/
86+
private Set<String> parseXMLFile() throws Exception {
87+
final ImmutableSet.Builder<String> sharedModulesBuilder = ImmutableSet.builder();
88+
final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
89+
final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
90+
91+
try (final InputStream xmlFile = getXMLInputStream()) {
92+
final Document doc = dBuilder.parse(xmlFile);
93+
doc.getDocumentElement().normalize();
94+
95+
final NodeList nList = doc.getElementsByTagName("package");
96+
for (int i = 0; i < nList.getLength(); i++) {
97+
sharedModulesBuilder.add(nList.item(i).getFirstChild().getNodeValue());
98+
}
99+
}
100+
return sharedModulesBuilder.build();
101+
}
102+
103+
/**
104+
* Retrieves a {@link Set} with the name of the Python modules to be shared across sub-interpreters.
105+
*
106+
* @return The name of the Python modules to be shared across sub-interpreters.
107+
*/
108+
public Set<String> getSharedModules() {
109+
Set<String> sharedModules = ImmutableSet.of();
110+
try {
111+
sharedModules = parseXMLFile();
112+
} catch (final Exception e) {
113+
logger.warn("Problem while getting the XML file with the Python modules to be shared.", e);
114+
}
115+
return sharedModules;
116+
}
117+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) 2018 Feedzai
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+
/**
18+
* This package contains logic for parsing XML files to retrieve the name of the Python modules to be shared across
19+
* sub-interpreters.
20+
*
21+
* @since 0.1.5
22+
*/
23+
package com.feedzai.openml.python.modules;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2018 Feedzai
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+
package com.feedzai.openml.python.jep.instance;
18+
19+
import org.junit.After;
20+
import org.junit.Before;
21+
import org.junit.Test;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* Contains the tests for the wrapper for the Jep object.
27+
*
28+
* @author Paulo Pereira (paulo.pereira@feedzai.com)
29+
* @since 0.1.5
30+
*/
31+
public class JepInstanceTest {
32+
33+
/**
34+
* The wrapper for the Jep object used in the tests.
35+
*/
36+
private JepInstance jepInstance;
37+
38+
/**
39+
* Initializes an instance of {@link JepInstance}.
40+
*/
41+
@Before
42+
public void setUp() {
43+
this.jepInstance = new JepInstance();
44+
this.jepInstance.start();
45+
}
46+
47+
/**
48+
* Tears downs the instance of {@link JepInstance}.
49+
*/
50+
@After
51+
public void tearDown() {
52+
this.jepInstance.stop();
53+
}
54+
55+
/**
56+
* Tests the submission of evaluations on a {@link JepInstance}.
57+
*
58+
* @throws Exception If there is a problem while getting the result.
59+
*/
60+
@Test
61+
public void submitEvaluationTest() throws Exception {
62+
final String result = this.jepInstance.submitEvaluation((jep) -> jep.getValue("1 + 2")).get().toString();
63+
assertThat(result)
64+
.as("The result of the evaluation")
65+
.isEqualTo("3");
66+
}
67+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2018 Feedzai
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+
/**
18+
* This package contains the unit-tests for {@link com.feedzai.openml.python.jep.instance}.
19+
*
20+
* @since 0.1.5
21+
*/
22+
package com.feedzai.openml.python.jep.instance;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2018 Feedzai
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+
package com.feedzai.openml.python.modules;
18+
19+
import org.assertj.core.util.Files;
20+
import org.junit.Test;
21+
22+
import java.io.File;
23+
import java.util.Set;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests the retrieving of the modules to be shared across sub-interpreters from a XML file.
29+
*
30+
* @author Paulo Pereira (paulo.pereira@feedzai.com)
31+
* @since 0.1.5
32+
*/
33+
public class SharedModulesParserTest {
34+
35+
/**
36+
* Tests the retrieving of the shared modules from a valid XML file with name of two modules to be shared.
37+
*/
38+
@Test
39+
public void validXMLFileTest() {
40+
final Set<String> sharedPythonPackages = new SharedModulesParser().getSharedModules();
41+
assertThat(sharedPythonPackages)
42+
.as("Set of shared modules.")
43+
.hasSize(2)
44+
.contains("my_package_1", "my_package_2");
45+
}
46+
47+
/**
48+
* Tests the retrieving of the shared modules from an empty file.
49+
*/
50+
@Test
51+
public void emptyFileTest() {
52+
final File file = Files.newTemporaryFile();
53+
file.deleteOnExit();
54+
55+
final SharedModulesParser sharedModulesParser = new SharedModulesParser(file.getAbsolutePath());
56+
assertThat(sharedModulesParser.getSharedModules())
57+
.as("Set of shared modules.")
58+
.hasSize(0);
59+
}
60+
61+
/**
62+
* Tests the retrieving of the shared modules from a non existing file.
63+
*/
64+
@Test
65+
public void nonExistingFileTest() {
66+
final SharedModulesParser sharedModulesParser = new SharedModulesParser("non_existing_file");
67+
assertThat(sharedModulesParser.getSharedModules())
68+
.as("Set of shared modules.")
69+
.hasSize(0);
70+
}
71+
}

0 commit comments

Comments
 (0)