-
Notifications
You must be signed in to change notification settings - Fork 1
Add automatic allocation of copies of the share library on demand for multiple copies of players. #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add automatic allocation of copies of the share library on demand for multiple copies of players. #77
Changes from 10 commits
7981530
daa7431
0b2e8da
153b227
3a909cb
dc43053
299dd7e
401acaf
a2ab830
6d90fe5
235e59b
181c08c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from collections import defaultdict | ||
from ctypes import cdll | ||
from ctypes.util import find_library | ||
from multiprocessing.managers import BaseManager | ||
import os | ||
from pathlib import Path | ||
import platform | ||
import shutil | ||
import subprocess | ||
import tempfile | ||
import uuid | ||
|
||
|
||
def load_library(filename): | ||
"""Loads a shared library.""" | ||
lib = None | ||
if os.path.exists(filename): | ||
|
||
lib = cdll.LoadLibrary(filename) | ||
return lib | ||
|
||
|
||
class SharedLibraryManager(object): | ||
"""LibraryManager creates (and deletes) copies of a shared library, which | ||
enables multiple copies of the same strategy to be run without the end user | ||
having to maintain many copies of the shared library. | ||
|
||
This works by making a copy of the shared library file and loading it into | ||
memory again. Loading the same file again will return a reference to the | ||
same memory addresses. To be thread-safe, this class just passes filenames | ||
back to the Player class (which actually loads a reference to the library), | ||
ensuring that multiple copies of a given player type do not use the same | ||
copy of the shared library. | ||
""" | ||
|
||
def __init__(self, shared_library_name, verbose=False): | ||
self.shared_library_name = shared_library_name | ||
self.verbose = verbose | ||
self.filenames = [] | ||
self.player_indices = defaultdict(set) | ||
self.player_next = defaultdict(set) | ||
# Generate a random prefix for tempfile generation | ||
self.prefix = str(uuid.uuid4()) | ||
self.library_path = self.find_shared_library(shared_library_name) | ||
|
||
def find_shared_library(self, shared_library_name): | ||
# Hack for Linux since find_library doesn't return the full path. | ||
if 'Linux' in platform.system(): | ||
output = subprocess.check_output(["ldconfig", "-p"]) | ||
for line in str(output).split(r"\n"): | ||
rhs = line.split(" => ")[-1] | ||
if shared_library_name in rhs: | ||
return rhs | ||
raise ValueError("{} not found".format(shared_library_name)) | ||
else: | ||
return find_library( | ||
shared_library_name.replace("lib", "").replace(".so", "")) | ||
|
||
def create_library_copy(self): | ||
"""Create a new copy of the shared library.""" | ||
# Copy the library file to a new (temp) location. | ||
temp_directory = tempfile.gettempdir() | ||
copy_number = len(self.filenames) | ||
filename = "{}-{}-{}".format( | ||
self.prefix, | ||
str(copy_number), | ||
self.shared_library_name) | ||
new_filename = str(Path(temp_directory) / filename) | ||
|
||
if self.verbose: | ||
print("Loading {}".format(new_filename)) | ||
shutil.copy2(self.library_path, new_filename) | ||
self.filenames.append(new_filename) | ||
|
||
def next_player_index(self, name): | ||
"""Determine the index of the next free shared library copy to | ||
allocate for the player. If none is available then make another copy.""" | ||
# Is there a free index? | ||
if len(self.player_next[name]) > 0: | ||
return self.player_next[name].pop() | ||
# Do we need to load a new copy? | ||
player_count = len(self.player_indices[name]) | ||
if player_count == len(self.filenames): | ||
self.create_library_copy() | ||
return player_count | ||
# Find the first unused index | ||
for i in range(len(self.filenames)): | ||
if i not in self.player_indices[name]: | ||
return i | ||
raise ValueError("We shouldn't be here.") | ||
|
||
def get_filename_for_player(self, name): | ||
"""For a given player return a filename for a copy of the shared library | ||
for use in a Player class, along with an index for later releasing.""" | ||
index = self.next_player_index(name) | ||
self.player_indices[name].add(index) | ||
if self.verbose: | ||
print("allocating {}".format(index)) | ||
return index, self.filenames[index] | ||
|
||
def release(self, name, index): | ||
"""Release the copy of the library so that it can be re-allocated.""" | ||
self.player_indices[name].remove(index) | ||
if self.verbose: | ||
print("releasing {}".format(index)) | ||
self.player_next[name].add(index) | ||
|
||
def __del__(self): | ||
"""Cleanup temp files on object deletion.""" | ||
for filename in self.filenames: | ||
path = Path(filename) | ||
if path.exists(): | ||
if self.verbose: | ||
print("deleting", str(path)) | ||
os.remove(str(path)) | ||
|
||
|
||
|
||
# Setup up thread safe library manager. | ||
class MultiprocessManager(BaseManager): | ||
pass | ||
|
||
|
||
MultiprocessManager.register('SharedLibraryManager', SharedLibraryManager) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and I think this should now become redundant