Skip to content

Commit 1ca2e91

Browse files
authored
Merge pull request #77 from Axelrod-Python/indexing
WIP: Add automatic allocation of copies of the share library on demand for multiple copies of players.
2 parents f1b0990 + 181c08c commit 1ca2e91

File tree

5 files changed

+192
-32
lines changed

5 files changed

+192
-32
lines changed

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ before_install:
1010
- git clone https://github.com/Axelrod-Python/TourExec.git /tmp/TourExec
1111
- cd /tmp/TourExec
1212
- sudo make install
13-
- export LD_LIBRARY_PATH=/usr/local/lib
13+
- echo "/usr/lib/libstrategies.so" | sudo tee /etc/ld.so.conf.d/strategies-lib.conf
14+
- sudo ldconfig
15+
- ldconfig -p | grep libstrategies.so
1416
- cd $TRAVIS_BUILD_DIR
1517
install:
1618
- pip install -r requirements.txt

src/axelrod_fortran/player.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1+
from ctypes import byref, c_float, c_int, POINTER
12
import random
23
import warnings
34

45
import axelrod as axl
56
from axelrod.interaction_utils import compute_final_score
67
from axelrod.action import Action
7-
from ctypes import cdll, c_int, c_float, byref, POINTER
8+
89
from .strategies import characteristics
10+
from .shared_library_manager import MultiprocessManager, load_library
911

1012
C, D = Action.C, Action.D
1113
actions = {0: C, 1: D}
1214
original_actions = {C: 0, D: 1}
1315

1416

17+
self_interaction_message = """
18+
You are playing a match with the same player against itself. However
19+
axelrod_fortran players share memory. You can initialise another instance of an
20+
Axelrod_fortran player with player.clone().
21+
"""
22+
23+
24+
# Initialize a module-wide manager for loading copies of the shared library.
25+
manager = MultiprocessManager()
26+
manager.start()
27+
shared_library_manager = manager.SharedLibraryManager("libstrategies.so")
28+
29+
1530
class Player(axl.Player):
1631

1732
classifier = {"stochastic": True}
1833

19-
def __init__(self, original_name,
20-
shared_library_name='libstrategies.so'):
34+
def __init__(self, original_name):
2135
"""
2236
Parameters
2337
----------
@@ -28,14 +42,16 @@ def __init__(self, original_name,
2842
A instance of an axelrod Game
2943
"""
3044
super().__init__()
31-
self.shared_library_name = shared_library_name
32-
self.shared_library = cdll.LoadLibrary(shared_library_name)
45+
self.index, self.shared_library_filename = \
46+
shared_library_manager.get_filename_for_player(original_name)
47+
self.shared_library = load_library(self.shared_library_filename)
3348
self.original_name = original_name
3449
self.original_function = self.original_name
3550
is_stochastic = characteristics[self.original_name]['stochastic']
3651
if is_stochastic is not None:
3752
self.classifier['stochastic'] = is_stochastic
3853

54+
3955
def __enter__(self):
4056
return self
4157

@@ -75,17 +91,8 @@ def original_strategy(
7591
return self.original_function(*[byref(arg) for arg in args])
7692

7793
def strategy(self, opponent):
78-
if type(opponent) is Player \
79-
and (opponent.original_name == self.original_name) \
80-
and (opponent.shared_library_name == self.shared_library_name):
81-
82-
message = """
83-
You are playing a match with two copies of the same player.
84-
However the axelrod fortran players share memory.
85-
You can initialise an instance of an Axelrod_fortran player with a
86-
`shared_library_name`
87-
variable that points to a copy of the shared library."""
88-
warnings.warn(message=message)
94+
if self is opponent:
95+
warnings.warn(message=self_interaction_message)
8996

9097
if not self.history:
9198
their_last_move = 0
@@ -106,6 +113,25 @@ def strategy(self, opponent):
106113
my_last_move)
107114
return actions[original_action]
108115

116+
def _release_shared_library(self):
117+
# While this looks like we're checking that the shared library file
118+
# isn't deleted, the exception is actually thrown if the manager
119+
# thread closes before the player class is garbage collected, which
120+
# tends to happen at the end of a script.
121+
try:
122+
shared_library_manager.release(self.original_name, self.index)
123+
except FileNotFoundError:
124+
pass
125+
109126
def reset(self):
127+
# Release the shared library since the object is rebuilt on reset.
128+
self._release_shared_library()
110129
super().reset()
111130
self.original_function = self.original_name
131+
132+
def __del__(self):
133+
# Release the library before deletion.
134+
self._release_shared_library()
135+
136+
def __repr__(self):
137+
return self.original_name
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from collections import defaultdict
2+
from ctypes import cdll
3+
from ctypes.util import find_library
4+
from multiprocessing.managers import BaseManager
5+
from pathlib import Path
6+
import platform
7+
import shutil
8+
import subprocess
9+
import tempfile
10+
import uuid
11+
12+
13+
def load_library(filename):
14+
"""Loads a shared library."""
15+
lib = None
16+
if Path(filename).exists():
17+
lib = cdll.LoadLibrary(filename)
18+
return lib
19+
20+
21+
class SharedLibraryManager(object):
22+
"""LibraryManager creates (and deletes) copies of a shared library, which
23+
enables multiple copies of the same strategy to be run without the end user
24+
having to maintain many copies of the shared library.
25+
26+
This works by making a copy of the shared library file and loading it into
27+
memory again. Loading the same file again will return a reference to the
28+
same memory addresses. To be thread-safe, this class just passes filenames
29+
back to the Player class (which actually loads a reference to the library),
30+
ensuring that multiple copies of a given player type do not use the same
31+
copy of the shared library.
32+
"""
33+
34+
def __init__(self, shared_library_name, verbose=False):
35+
self.shared_library_name = shared_library_name
36+
self.verbose = verbose
37+
self.filenames = []
38+
self.player_indices = defaultdict(set)
39+
self.player_next = defaultdict(set)
40+
# Generate a random prefix for tempfile generation
41+
self.prefix = str(uuid.uuid4())
42+
self.library_path = self.find_shared_library(shared_library_name)
43+
44+
def find_shared_library(self, shared_library_name):
45+
# Hack for Linux since find_library doesn't return the full path.
46+
if 'Linux' in platform.system():
47+
output = subprocess.check_output(["ldconfig", "-p"])
48+
for line in str(output).split(r"\n"):
49+
rhs = line.split(" => ")[-1]
50+
if shared_library_name in rhs:
51+
return rhs
52+
raise ValueError("{} not found".format(shared_library_name))
53+
else:
54+
return find_library(
55+
shared_library_name.replace("lib", "").replace(".so", ""))
56+
57+
def create_library_copy(self):
58+
"""Create a new copy of the shared library."""
59+
# Copy the library file to a new (temp) location.
60+
temp_directory = tempfile.gettempdir()
61+
copy_number = len(self.filenames)
62+
filename = "{}-{}-{}".format(
63+
self.prefix,
64+
str(copy_number),
65+
self.shared_library_name)
66+
new_filename = str(Path(temp_directory, filename))
67+
if self.verbose:
68+
print("Loading {}".format(new_filename))
69+
shutil.copy2(self.library_path, new_filename)
70+
self.filenames.append(new_filename)
71+
72+
def next_player_index(self, name):
73+
"""Determine the index of the next free shared library copy to
74+
allocate for the player. If none is available then make another copy."""
75+
# Is there a free index?
76+
if len(self.player_next[name]) > 0:
77+
return self.player_next[name].pop()
78+
# Do we need to load a new copy?
79+
player_count = len(self.player_indices[name])
80+
if player_count == len(self.filenames):
81+
self.create_library_copy()
82+
return player_count
83+
# Find the first unused index
84+
for i in range(len(self.filenames)):
85+
if i not in self.player_indices[name]:
86+
return i
87+
raise ValueError("We shouldn't be here.")
88+
89+
def get_filename_for_player(self, name):
90+
"""For a given player return a filename for a copy of the shared library
91+
for use in a Player class, along with an index for later releasing."""
92+
index = self.next_player_index(name)
93+
self.player_indices[name].add(index)
94+
if self.verbose:
95+
print("allocating {}".format(index))
96+
return index, self.filenames[index]
97+
98+
def release(self, name, index):
99+
"""Release the copy of the library so that it can be re-allocated."""
100+
self.player_indices[name].remove(index)
101+
if self.verbose:
102+
print("releasing {}".format(index))
103+
self.player_next[name].add(index)
104+
105+
def __del__(self):
106+
"""Cleanup temp files on object deletion."""
107+
for filename in self.filenames:
108+
path = Path(filename)
109+
if path.exists():
110+
if self.verbose:
111+
print("deleting", str(path))
112+
path.unlink()
113+
114+
115+
# Setup up thread safe library manager.
116+
class MultiprocessManager(BaseManager):
117+
pass
118+
119+
120+
MultiprocessManager.register('SharedLibraryManager', SharedLibraryManager)

tests/test_player.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from axelrod_fortran import Player, characteristics, all_strategies
2-
from axelrod import (Alternator, Cooperator, Defector,
3-
Match, Game, basic_strategies, seed)
4-
from axelrod.action import Action
51
from ctypes import c_int, c_float, POINTER, CDLL
6-
72
import itertools
3+
84
import pytest
95

6+
from axelrod_fortran import Player, characteristics, all_strategies
7+
from axelrod import (Alternator, Cooperator, Defector, Match, MoranProcess,
8+
Game, basic_strategies, seed)
9+
from axelrod.action import Action
10+
11+
1012
C, D = Action.C, Action.D
1113

1214

@@ -22,15 +24,6 @@ def test_init():
2224
assert player.original_function.restype == c_int
2325
with pytest.raises(ValueError):
2426
player = Player('test')
25-
assert "libstrategies.so" == player.shared_library_name
26-
assert type(player.shared_library) is CDLL
27-
assert "libstrategies.so" in str(player.shared_library)
28-
29-
def test_init_with_shared():
30-
player = Player("k42r", shared_library_name="libstrategies.so")
31-
assert "libstrategies.so" == player.shared_library_name
32-
assert type(player.shared_library) is CDLL
33-
assert "libstrategies.so" in str(player.shared_library)
3427

3528

3629
def test_matches():
@@ -106,6 +99,7 @@ def test_original_strategy():
10699
my_score += scores[0]
107100
their_score += scores[1]
108101

102+
109103
def test_deterministic_strategies():
110104
"""
111105
Test that the strategies classified as deterministic indeed act
@@ -139,6 +133,7 @@ def test_implemented_strategies():
139133
axl_match = Match((axl_player, opponent))
140134
assert interactions == axl_match.play(), (player, opponent)
141135

136+
142137
def test_champion_v_alternator():
143138
"""
144139
Specific regression test for a bug.
@@ -155,18 +150,20 @@ def test_champion_v_alternator():
155150
seed(0)
156151
assert interactions == match.play()
157152

153+
158154
def test_warning_for_self_interaction(recwarn):
159155
"""
160156
Test that a warning is given for a self interaction.
161157
"""
162158
player = Player("k42r")
163-
opponent = Player("k42r")
159+
opponent = player
164160

165161
match = Match((player, opponent))
166162

167163
interactions = match.play()
168164
assert len(recwarn) == 1
169165

166+
170167
def test_no_warning_for_normal_interaction(recwarn):
171168
"""
172169
Test that a warning is not given for a normal interaction
@@ -180,3 +177,11 @@ def test_no_warning_for_normal_interaction(recwarn):
180177

181178
interactions = match.play()
182179
assert len(recwarn) == 0
180+
181+
182+
def test_multiple_copies(recwarn):
183+
players = [Player('ktitfortatc') for _ in range(5)] + [
184+
Player('k42r') for _ in range(5)]
185+
mp = MoranProcess(players)
186+
mp.play()
187+
mp.populations_plot()

tests/test_titfortat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ def test_versus_defector():
2424
match = axl.Match(players, 5)
2525
expected = [(C, D), (D, D), (D, D), (D, D), (D, D)]
2626
assert match.play() == expected
27+
28+
29+
def test_versus_itself():
30+
players = (Player('ktitfortatc'), Player('ktitfortatc'))
31+
match = axl.Match(players, 5)
32+
expected = [(C, C), (C, C), (C, C), (C, C), (C, C)]
33+
assert match.play() == expected

0 commit comments

Comments
 (0)