Merge lp:~gagern/bzr/bug396819-lazy_import-threadsafe into lp:bzr

Proposed by Martin von Gagern
Status: Merged
Merged at revision: 6426
Proposed branch: lp:~gagern/bzr/bug396819-lazy_import-threadsafe
Merge into: lp:bzr
Diff against target: 304 lines (+115/-82)
3 files modified
bzrlib/builtins.py (+11/-2)
bzrlib/lazy_import.py (+67/-53)
bzrlib/tests/test_lazy_import.py (+37/-27)
To merge this branch: bzr merge lp:~gagern/bzr/bug396819-lazy_import-threadsafe
Reviewer Review Type Date Requested Status
Martin Packman (community) Needs Fixing
Jelmer Vernooij (community) Abstain
Vincent Ladeuil Needs Information
John A Meinel Pending
Benji York Pending
Review via email: mp+73475@code.launchpad.net

Commit message

Make lazy imports (more) thread-safe

Description of the change

I know this still lacks a news entry, but apart from that, would you be willing to merge something along these lines to make lazy imports properly thread safe?

As I argued in bug #396819, I believe that you cannot have guaranteed tread-safe lazy imports without allowing the same replacer to resolve to the real object an arbitrary number of times. And you cannot avoid duplicate calls to the fatory (i.e. duplicate imports) unless you want to use locks.

The attached code avoids locks, as concurrent replacements should be rare and as duplicate imports should be harmless except for a slight cpu time penalty to detect it. It simplifies the replacer class by introducing a _resolve method which always returns the actual object.

As there are no longer any errors when the replace code is called repeatedly, we might miss the fact that the replacer object itself has been copied to some other name. To deal with this problem, the old error-raising behaviour will be restored for selftests, thus ensuring that the code is clean in that respect. Obviously, when the selftests reach the part where the thread safety of the replacer is tested, those checks have to be disabled again...

If you want any kind of beautification, name changes, or whatever, simply ask me or feel free to branch this yourself and apply changes as you see fit. I'm only proposing the general idea as a kind of proof of concept, and thus am most interested in your overall general opinion.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

Thanks for working on this !

This looks promising and I won't mind polishing it as patch pilot
(or helping you) but before digging I'd like some feedback from
jam and benji which have been involved recently.

[feedback] Overall, the approach sounds good. I'm not sure I'm
reading this correctly but it seems you've now made
ScopeReplacer._replace() the only place where races can occur. If
that's the case and we want to lock something, that should
probably be there.

[nit]

17 +
18 + disallow_proxying()

A bit naked ;) A comment explaining why it's done here (as early
as possible but no sooner) will be good.

[needsinfo]

218 - if event == 'line' and 'lazy_import.py' in filename:
219 - line = linecache.getline(filename, frame.f_lineno)
220 - # ...and the line of code is the one we're looking for...
221 - if 'del self._factory' in line:
<...>
228 + if (filename.endswith('lazy_import.py') and
229 + function_name == self.method_to_trace):

This looks nice and less hackish, but I thought you were relying
on the code you're replacing here in lp:~gagern/bzr/bug-702914 ?

I wouldn't mind merging these tests instead (or their moral equivalent).

review: Needs Information
Revision history for this message
Martin von Gagern (gagern) wrote :
Download full text (3.5 KiB)

On 01.09.2011 10:11, Vincent Ladeuil wrote:
> [feedback] Overall, the approach sounds good. I'm not sure I'm
> reading this correctly but it seems you've now made
> ScopeReplacer._replace() the only place where races can occur. If
> that's the case and we want to lock something, that should
> probably be there.

Depends on what race you're talking about.

A race resulting in double import will occur if the second thread reads
the _real_obj member before the first thread has written it. As the read
of _real_obj occurs in _resolve, that's involved in the race as well,
and a lock should encompass that whole code section, i.e. be aquired
before _real_obj is first accessed.

A race resulting in an UNDETECTED double replacement is restricted to
the very end of _replace, starting at the second read for _real_obj, the
one to the prev_obj local variable.

Reducing the chances for the second kind of race is the only reason for
the duplicate read of _real_obj. The assumption is that importing a
module might take considerable time, so a critical code section only
after the import represents a much shorter timespan.

> [nit]
>
> 17 +
> 18 + disallow_proxying()
>
> A bit naked ;) A comment explaining why it's done here (as early
> as possible but no sooner) will be good.

Guess you're right. To make things even cleaner, I could import the
lazy_import module instead of the function of the same name. That would
make this call use a qualified name, giving it more context. Will do
both soon.

> [needsinfo]
>
> 218 - if event == 'line' and 'lazy_import.py' in filename:
> 219 - line = linecache.getline(filename, frame.f_lineno)
> 220 - # ...and the line of code is the one we're looking for...
> 221 - if 'del self._factory' in line:
> <...>
> 228 + if (filename.endswith('lazy_import.py') and
> 229 + function_name == self.method_to_trace):
>
> This looks nice and less hackish, but I thought you were relying
> on the code you're replacing here in lp:~gagern/bzr/bug-702914 ?

This branch here supersedes that one, in my opinion. Notice that I never
requested that one to be merged. It was only intended as a proof that
the race problems hadn't been completely resolved yet. And the fact that
problems remained despite the original test cases shows another fact:
you cannot reasonably test for all possible races, but only those you
can think of. Thus the more people reviewing the code and thinking of
possible races, the more likely they won't miss one. The number of test
cases alone does little to increase confidence.

> I wouldn't mind merging these tests instead (or their moral equivalent).

There is no direct equivalent, as the lines of code mentioned no longer
exist in that fashion. I also came to believe that the scenarios
approach was bad, as it implied that all three kinds of test could be
matched by the same break point line regexp, which isn't necessarily
obvious.

The tests here are imo suitable to guard against a wide range of likely
races. The fact that the method name is an argument makes adding more
tests easy. So I'd rather prefer these tests here over the ones from
lp:~gagern/bzr/bug-702914 . I'd be happy to add three more tests for
_replace instead of ...

Read more...

Revision history for this message
Martin von Gagern (gagern) wrote :

Ping?

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On Tue 06 Sep 2011 20:53:04 CEST, Martin von Gagern wrote:
> Ping?
John is sprinting this week so it might take a while longer before he
has time to look at this.

Cheers,

Jelmer

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Abstain
Revision history for this message
Martin von Gagern (gagern) wrote :

Second ping? Merge request has been out more than a month, please don't forget about it!

Revision history for this message
Martin Packman (gz) wrote :

Sorry we've not managed to get this reviewed MvG. Your analysis here and in the bug about the previous change seem correct to me, but I'm unconvinced about making further changes to lazy_import in this regard.

I can't offer useful suggestions for improving the branch, only complain about the state of the pitch:

* There's no way to disable lazy imports for things like loggerhead that care about threading not startup time.

* Telling whether a given lazy import is actually doing anything useful is far too hard.

  * It's not uncommon to see an object get imported then unconditionally used further down in the module.

  * Many parts of bzrlib are lazily imported somewhere, but end up always being required anyway.

  * The import tariff tests are nice, but very heavy compared to say, a lint style check.

* Parsing import statements out of strings is silly in many respects.

What I'd like to see would be a replacement with fewer features bzrlib could move to instead. I'm imagining it would:

* Use the import statement directly.

* Only works on module objects.

* Use a proxy that remains in place but gains attributes.

Since bzrlib.lazy_import was introduced we've gained a bunch of other lazy mechanisms which take over some of the work, but it's all very ad hoc.

Revision history for this message
Martin von Gagern (gagern) wrote :

Am 23.11.11 15:19, schrieb Martin Packman:
> Sorry we've not managed to get this reviewed MvG. Your analysis here and in the bug about the previous change seem correct to me, but I'm unconvinced about making further changes to lazy_import in this regard.

Currently, lazy_import is not merely ill designed, but actually breaking
stuff. So I believe that this proposal still should receive a proper
review, and the fix be committed. Unless there actually is code ready to
replace it, fixing it is better than not fixing it.

> I can't offer useful suggestions for improving the branch, only complain about the state of the pitch:
> * There's no way to disable lazy imports for things like loggerhead that care about threading not startup time.

Would be easy, but a different issue, to be implemented in a different
branch, preferrably on top of this one.

> * Telling whether a given lazy import is actually doing anything useful is far too hard.
> * It's not uncommon to see an object get imported then unconditionally used further down in the module.
> * Many parts of bzrlib are lazily imported somewhere, but end up always being required anyway.
> * The import tariff tests are nice, but very heavy compared to say, a lint style check.

tariff tests can deal with only a very limited number of scenarios to
check, but will by design also catch obscure paths leading to some
import. Lint-style checks could process a lot more code paths, detecting
lots of the stuff you describe above, but would be likely to miss some
more obscure constructs. So I'd consider them complementary.

> What I'd like to see would be a replacement with fewer features bzrlib could move to instead. I'm imagining it would:
>
> * Use the import statement directly.

I know that strange things are possible, but how would this work?
"from bzrlib.lazyly import foobar", with "lazily" not being a module but
instead an object working as a proxy factory? I guess that should be
possible, although I have some concerns about nested modules.

> * Only works on module objects.

That would surely make things a lot easier and cleaner.

> * Use a proxy that remains in place but gains attributes.

Not sure about performance here. I can imagine why a self-replacing
proxy would at least feel faster. It should be possible to do some
benchmarks on this.

Revision history for this message
Martin Packman (gz) wrote :

> Currently, lazy_import is not merely ill designed, but actually breaking
> stuff. So I believe that this proposal still should receive a proper
> review, and the fix be committed. Unless there actually is code ready to
> replace it, fixing it is better than not fixing it.

That's fair. Between me and Jelmer I'll see if we can manage a review here.

I'll leave the discussion of alternatives to a later merge proposal. :)

Revision history for this message
Martin Packman (gz) wrote :

Okay, Jelmer and I have discussed the general issue and the approach this branch takes. So, the code here is aimed at removing cases that result in exceptions, either usefully (in the case of multiply assigned proxies), or incorrectly (in the case of thread races). On that basis, the branch should land, and soon so it has some time on trunk to shake out any further issues.

The disallow_proxying mechanism for keeping the old testing for accidentally assigning a lazy object before it gets thunked I'm not convinced should be included with this change.

+ ScopeReplacer._last_duplicate_replacement = self

This is module-global state which is only used so the one call point of disallow_proxying() in selftest can retroactively raise (the most recent occurrence of) the error. Given that bzrlib.tests and test modules do not lazily import the will have already pulled in many things (varying if -s is used), this will only test one real import path.

For this branch, I suggest pulling out that code and living with the reduced coverage of assignment accidents, and just using `self.overrideAttr(ScopeReplacer, "_should_proxy", False)` for tests.

To get coverage for import issues again, using a debug flag to set the _should_proxy value and something similar to the import tarrif tests would work, and should be done in a future branch.

+ The loosing caller used to see an exception (bugs 396819 and 702914).
...
+ in order win the race, setting up the original caller to loose.

When changing this docstring it'd be nice to fix the spelling of 'lose'. :)

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2011-08-31 15:25:11 +0000
+++ bzrlib/builtins.py 2011-09-01 20:49:31 +0000
@@ -18,8 +18,8 @@
1818
19import os19import os
2020
21from bzrlib.lazy_import import lazy_import21from bzrlib import lazy_import
22lazy_import(globals(), """22lazy_import.lazy_import(globals(), """
23import cStringIO23import cStringIO
24import sys24import sys
25import time25import time
@@ -3800,6 +3800,15 @@
3800 randomize=None, exclude=None, strict=False,3800 randomize=None, exclude=None, strict=False,
3801 load_list=None, debugflag=None, starting_with=None, subunit=False,3801 load_list=None, debugflag=None, starting_with=None, subunit=False,
3802 parallel=None, lsprof_tests=False):3802 parallel=None, lsprof_tests=False):
3803
3804 # During selftest, disallow proxying, as it can cause severe
3805 # performance penalties and is only needed for thread
3806 # safety. The selftest command is assumed to not use threads
3807 # too heavily. The call should be as early as possible, as
3808 # error reporting for past duplicate imports won't have useful
3809 # backtraces.
3810 lazy_import.disallow_proxying()
3811
3803 from bzrlib import tests3812 from bzrlib import tests
38043813
3805 if testspecs_list is not None:3814 if testspecs_list is not None:
38063815
=== modified file 'bzrlib/lazy_import.py'
--- bzrlib/lazy_import.py 2011-08-19 18:02:37 +0000
+++ bzrlib/lazy_import.py 2011-09-01 20:49:31 +0000
@@ -51,10 +51,15 @@
5151
52 __slots__ = ('_scope', '_factory', '_name', '_real_obj')52 __slots__ = ('_scope', '_factory', '_name', '_real_obj')
5353
54 # Setting this to True will allow you to do x = y, and still access members54 # This will record the last duplicate replacement. It is used
55 # from both variables. This should not normally be enabled, but is useful55 # during selftests where duplicate replacements are forbidden.
56 # when building documentation.56 _last_duplicate_replacement = None
57 _should_proxy = False57
58 # If you to do x = y, setting this to False will disallow access to
59 # members from the second variable (i.e. x). This should normally
60 # be enabled for reasons of thread safety and documentation, but
61 # will be disabled during the selftest command to check for abuse.
62 _should_proxy = True
5863
59 def __init__(self, scope, factory, name):64 def __init__(self, scope, factory, name):
60 """Create a temporary object in the specified scope.65 """Create a temporary object in the specified scope.
@@ -71,74 +76,83 @@
71 object.__setattr__(self, '_real_obj', None)76 object.__setattr__(self, '_real_obj', None)
72 scope[name] = self77 scope[name] = self
7378
79 def _resolve(self):
80 """Return the real object for which this is a placeholder"""
81 real_obj = object.__getattribute__(self, '_real_obj')
82 if real_obj is not None:
83 object.__getattribute__(self, '_duplicate_replacement')()
84 return real_obj
85 replace = object.__getattribute__(self, '_replace')
86 return replace()
87
74 def _replace(self):88 def _replace(self):
75 """Actually replace self with other in the given scope"""89 """Actually replace self with other in the given scope"""
76 name = object.__getattribute__(self, '_name')90 name = object.__getattribute__(self, '_name')
77 try:91 factory = object.__getattribute__(self, '_factory')
78 factory = object.__getattribute__(self, '_factory')92 scope = object.__getattribute__(self, '_scope')
79 scope = object.__getattribute__(self, '_scope')
80 except AttributeError, e:
81 # Because ScopeReplacer objects only replace a single
82 # item, passing them to another variable before they are
83 # replaced would cause them to keep getting replaced
84 # (only they are replacing the wrong variable). So we
85 # make it forbidden, and try to give a good error.
86 raise errors.IllegalUseOfScopeReplacer(
87 name, msg="Object already cleaned up, did you assign it"
88 " to another variable?",
89 extra=e)
90 obj = factory(self, scope, name)93 obj = factory(self, scope, name)
91 if obj is self:94 if obj is self:
92 raise errors.IllegalUseOfScopeReplacer(name, msg="Object tried"95 raise errors.IllegalUseOfScopeReplacer(name, msg="Object tried"
93 " to replace itself, check it's not using its own scope.")96 " to replace itself, check it's not using its own scope.")
94 if ScopeReplacer._should_proxy:97
98 # The same object might be replaced repeatedly due to a race
99 # condition. We want to detect this fact for possible reporting.
100 prev_obj = object.__getattribute__(self, '_real_obj')
101 if prev_obj is None:
95 object.__setattr__(self, '_real_obj', obj)102 object.__setattr__(self, '_real_obj', obj)
96 scope[name] = obj103 scope[name] = obj
104 else:
105 # This indicates a duplicate replacement and should never
106 # happen except under rare race conditions. See lp:396819.
107 # Note: the above check is not thread-safe, so there is a
108 # slight chance that concurrent duplicate replacements are
109 # missed. Not worth the cost of a lock, though.
110 object.__getattribute__(self, '_duplicate_replacement')()
97 return obj111 return obj
98112
99 def _cleanup(self):
100 """Stop holding on to all the extra stuff"""
101 try:
102 del self._factory
103 except AttributeError:
104 # Oops, we just lost a race with another caller of _cleanup. Just
105 # ignore it.
106 pass
107
108 try:
109 del self._scope
110 except AttributeError:
111 # Another race loss. See above.
112 pass
113
114 # We keep _name, so that we can report errors
115 # del self._name
116
117 def __getattribute__(self, attr):113 def __getattribute__(self, attr):
118 obj = object.__getattribute__(self, '_real_obj')114 obj = object.__getattribute__(self, '_resolve')()
119 if obj is None:
120 _replace = object.__getattribute__(self, '_replace')
121 obj = _replace()
122 _cleanup = object.__getattribute__(self, '_cleanup')
123 _cleanup()
124 return getattr(obj, attr)115 return getattr(obj, attr)
125116
126 def __setattr__(self, attr, value):117 def __setattr__(self, attr, value):
127 obj = object.__getattribute__(self, '_real_obj')118 obj = object.__getattribute__(self, '_resolve')()
128 if obj is None:
129 _replace = object.__getattribute__(self, '_replace')
130 obj = _replace()
131 _cleanup = object.__getattribute__(self, '_cleanup')
132 _cleanup()
133 return setattr(obj, attr, value)119 return setattr(obj, attr, value)
134120
135 def __call__(self, *args, **kwargs):121 def __call__(self, *args, **kwargs):
136 _replace = object.__getattribute__(self, '_replace')122 obj = object.__getattribute__(self, '_resolve')()
137 obj = _replace()
138 _cleanup = object.__getattribute__(self, '_cleanup')
139 _cleanup()
140 return obj(*args, **kwargs)123 return obj(*args, **kwargs)
141124
125 def _duplicate_replacement(self):
126 if object.__getattribute__(self, '_should_proxy'):
127 # Do not report now, but remember in case we want to
128 # report this later.
129 ScopeReplacer._last_duplicate_replacement = self
130 else:
131 name = object.__getattribute__(self, '_name')
132 raise errors.IllegalUseOfScopeReplacer(
133 name, msg="Object already replaced, did you assign it"
134 " to another variable?")
135
136
137def disallow_proxying():
138 """Disallow lazily imported modules to be used as proxies.
139
140 Calling this function might cause problems with concurrent imports
141 in multithreaded environments, but will help detecting wasteful
142 indirection, so it should be called when executing unit tests.
143
144 Calling this function will even act for the most recent past case
145 of proxying, reporting the name of the proxied object. So a
146 successful call to this function will ensure that scope replacer
147 objects were never misused as proxies in the past and will never
148 be misused in the future."""
149
150 ScopeReplacer._should_proxy = False
151 last = ScopeReplacer._last_duplicate_replacement
152 if last is not None:
153 ScopeReplacer._last_duplicate_replacement = None
154 object.__getattribute__(last, '_duplicate_replacement')()
155
142156
143class ImportReplacer(ScopeReplacer):157class ImportReplacer(ScopeReplacer):
144 """This is designed to replace only a portion of an import list.158 """This is designed to replace only a portion of an import list.
145159
=== modified file 'bzrlib/tests/test_lazy_import.py'
--- bzrlib/tests/test_lazy_import.py 2011-08-19 18:19:31 +0000
+++ bzrlib/tests/test_lazy_import.py 2011-09-01 20:49:31 +0000
@@ -375,7 +375,6 @@
375 ('foo', 2),375 ('foo', 2),
376 ('foo', 3),376 ('foo', 3),
377 ('__getattribute__', 'foo'),377 ('__getattribute__', 'foo'),
378 '_replace',
379 ], actions)378 ], actions)
380379
381 def test_enable_proxying(self):380 def test_enable_proxying(self):
@@ -1171,51 +1170,62 @@
1171 ], self.actions)1170 ], self.actions)
11721171
11731172
1173class ProxyingReplacer(lazy_import.ScopeReplacer):
1174 """Replacer class that will do proxying even during selftest.
1175
1176 This is useful to check that the lazy import code is sufficiently
1177 thread-safe.
1178 """
1179
1180 _should_proxy = True
1181
1182
1174class TestScopeReplacerReentrance(TestCase):1183class TestScopeReplacerReentrance(TestCase):
1175 """The ScopeReplacer should be reentrant.1184 """The ScopeReplacer should be reentrant.
11761185
1177 Invoking a replacer while an invocation was already on-going leads to a1186 Invoking a replacer while an invocation was already on-going leads to a
1178 race to see which invocation will be the first to delete the _factory and1187 race to see which invocation will be the first to call _replace.
1179 _scope attributes. The loosing caller used to see AttributeErrors (bug1188 The loosing caller used to see an exception (bugs 396819 and 702914).
1180 702914).
11811189
1182 These tests set up a tracer that stops at the moment just before one of1190 These tests set up a tracer that stops at a suitable moment (upon
1183 the attributes is being deleted and starts another call to the1191 entry of a specified method) and starts another call to the
1184 functionality in question (__call__, __getattribute__, __setattr_) in1192 functionality in question (__call__, __getattribute__, __setattr_)
1185 order win the race, setting up the originall caller to loose.1193 in order win the race, setting up the original caller to loose.
1186 """1194 """
11871195
1188 def tracer(self, frame, event, arg):1196 def tracer(self, frame, event, arg):
1197 if event != 'call':
1198 return self.tracer
1189 # Grab the name of the file that contains the code being executed.1199 # Grab the name of the file that contains the code being executed.
1190 filename = frame.f_globals["__file__"]1200 code = frame.f_code
1201 filename = code.co_filename
1191 # Convert ".pyc" and ".pyo" file names to their ".py" equivalent.1202 # Convert ".pyc" and ".pyo" file names to their ".py" equivalent.
1192 filename = re.sub(r'\.py[co]$', '.py', filename)1203 filename = re.sub(r'\.py[co]$', '.py', filename)
1204 function_name = code.co_name
1193 # If we're executing a line of code from the right module...1205 # If we're executing a line of code from the right module...
1194 if event == 'line' and 'lazy_import.py' in filename:1206 if (filename.endswith('lazy_import.py') and
1195 line = linecache.getline(filename, frame.f_lineno)1207 function_name == self.method_to_trace):
1196 # ...and the line of code is the one we're looking for...1208 # We don't need to trace any more.
1197 if 'del self._factory' in line:1209 sys.settrace(None)
1198 # We don't need to trace any more.1210 # Run another racer. This one will "win" the race.
1199 sys.settrace(None)1211 self.racer()
1200 # Run another racer. This one will "win" the race, deleting
1201 # the attributes. When the first racer resumes it will loose
1202 # the race, generating an AttributeError.
1203 self.racer()
1204 return self.tracer1212 return self.tracer
12051213
1206 def run_race(self, racer):1214 def run_race(self, racer, method_to_trace='_resolve'):
1215 lazy_import.ScopeReplacer._should_proxy = True
1207 self.racer = racer1216 self.racer = racer
1217 self.method_to_trace = method_to_trace
1208 sys.settrace(self.tracer)1218 sys.settrace(self.tracer)
1209 self.racer() # Should not raise an AttributeError1219 self.racer() # Should not raise any exception
1210 # Make sure the tracer actually found the code it was looking for. If1220 # Make sure the tracer actually found the code it was
1211 # not, maybe the code was refactored in such a way that these tests1221 # looking for. If not, maybe the code was refactored in
1212 # aren't needed any more.1222 # such a way that these tests aren't needed any more.
1213 self.assertEqual(None, sys.gettrace())1223 self.assertEqual(None, sys.gettrace())
12141224
1215 def test_call(self):1225 def test_call(self):
1216 def factory(*args):1226 def factory(*args):
1217 return factory1227 return factory
1218 replacer = lazy_import.ScopeReplacer({}, factory, 'name')1228 replacer = ProxyingReplacer({}, factory, 'name')
1219 self.run_race(replacer)1229 self.run_race(replacer)
12201230
1221 def test_setattr(self):1231 def test_setattr(self):
@@ -1225,7 +1235,7 @@
1225 def factory(*args):1235 def factory(*args):
1226 return Replaced()1236 return Replaced()
12271237
1228 replacer = lazy_import.ScopeReplacer({}, factory, 'name')1238 replacer = ProxyingReplacer({}, factory, 'name')
12291239
1230 def racer():1240 def racer():
1231 replacer.foo = 421241 replacer.foo = 42
@@ -1239,7 +1249,7 @@
1239 def factory(*args):1249 def factory(*args):
1240 return Replaced()1250 return Replaced()
12411251
1242 replacer = lazy_import.ScopeReplacer({}, factory, 'name')1252 replacer = ProxyingReplacer({}, factory, 'name')
12431253
1244 def racer():1254 def racer():
1245 replacer.foo1255 replacer.foo