| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| """Test for Shared class.""" | |
| import gc | |
| import threading | |
| import time | |
| import unittest | |
| from apache_beam.utils import shared | |
| class Count(object): | |
| def __init__(self): | |
| self._lock = threading.Lock() | |
| self._total = 0 | |
| self._active = 0 | |
| def add_ref(self): | |
| with self._lock: | |
| self._total += 1 | |
| self._active += 1 | |
| def release_ref(self): | |
| with self._lock: | |
| self._active -= 1 | |
| def get_active(self): | |
| with self._lock: | |
| return self._active | |
| def get_total(self): | |
| with self._lock: | |
| return self._total | |
| class Marker(object): | |
| def __init__(self, count): | |
| self._count = count | |
| self._count.add_ref() | |
| def __del__(self): | |
| self._count.release_ref() | |
| class NamedObject(object): | |
| def __init__(self, name): | |
| self._name = name | |
| def get_name(self): | |
| return self._name | |
| class Sequence(object): | |
| def __init__(self): | |
| self._sequence = 0 | |
| def make_acquire_fn(self): | |
| # Every time acquire_fn is called, increases the sequence number and returns | |
| # a NamedObject with that sequenece number. | |
| def acquire_fn(): | |
| self._sequence += 1 | |
| return NamedObject('sequence%d' % self._sequence) | |
| return acquire_fn | |
| class SharedTest(unittest.TestCase): | |
| def testKeepalive(self): | |
| count = Count() | |
| shared_handle = shared.Shared() | |
| other_shared_handle = shared.Shared() | |
| def dummy_acquire_fn(): | |
| return None | |
| def acquire_fn(): | |
| return Marker(count) | |
| p1 = shared_handle.acquire(acquire_fn) | |
| self.assertEqual(1, count.get_total()) | |
| self.assertEqual(1, count.get_active()) | |
| del p1 | |
| gc.collect() | |
| # Won't be garbage collected, because of the keep-alive | |
| self.assertEqual(1, count.get_active()) | |
| # Reacquire. | |
| p2 = shared_handle.acquire(acquire_fn) | |
| self.assertEqual(1, count.get_total()) # No reinitialisation. | |
| self.assertEqual(1, count.get_active()) | |
| # Get rid of the keepalive | |
| other_shared_handle.acquire(dummy_acquire_fn) | |
| del p2 | |
| gc.collect() | |
| self.assertEqual(0, count.get_active()) | |
| def testMultiple(self): | |
| count = Count() | |
| shared_handle = shared.Shared() | |
| other_shared_handle = shared.Shared() | |
| def dummy_acquire_fn(): | |
| return None | |
| def acquire_fn(): | |
| return Marker(count) | |
| p = shared_handle.acquire(acquire_fn) | |
| other_shared_handle.acquire(dummy_acquire_fn) # Get rid of the keepalive | |
| self.assertEqual(1, count.get_total()) | |
| self.assertEqual(1, count.get_active()) | |
| del p | |
| gc.collect() | |
| self.assertEqual(0, count.get_active()) | |
| # Shared value should be garbage collected. | |
| # Acquiring multiple times only results in one initialisation | |
| p1 = shared_handle.acquire(acquire_fn) | |
| # Since shared value was released, expect a reinitialisation. | |
| self.assertEqual(2, count.get_total()) | |
| self.assertEqual(1, count.get_active()) | |
| p2 = shared_handle.acquire(acquire_fn) | |
| self.assertEqual(2, count.get_total()) | |
| self.assertEqual(1, count.get_active()) | |
| other_shared_handle.acquire(dummy_acquire_fn) # Get rid of the keepalive | |
| # Check that shared object isn't destroyed if there's still a reference to | |
| # it. | |
| del p2 | |
| gc.collect() | |
| self.assertEqual(1, count.get_active()) | |
| del p1 | |
| gc.collect() | |
| self.assertEqual(0, count.get_active()) | |
| def testConcurrentCallsDeduped(self): | |
| # Test that only one among many calls to acquire will actually run the | |
| # initialisation function. | |
| count = Count() | |
| shared_handle = shared.Shared() | |
| other_shared_handle = shared.Shared() | |
| refs = [] | |
| ref_lock = threading.Lock() | |
| def dummy_acquire_fn(): | |
| return None | |
| def acquire_fn(): | |
| time.sleep(1) | |
| return Marker(count) | |
| def thread_fn(): | |
| p = shared_handle.acquire(acquire_fn) | |
| with ref_lock: | |
| refs.append(p) | |
| threads = [] | |
| for _ in range(100): | |
| t = threading.Thread(target=thread_fn) | |
| threads.append(t) | |
| t.start() | |
| for t in threads: | |
| t.join() | |
| self.assertEqual(1, count.get_total()) | |
| self.assertEqual(1, count.get_active()) | |
| other_shared_handle.acquire(dummy_acquire_fn) # Get rid of the keepalive | |
| with ref_lock: | |
| del refs[:] | |
| gc.collect() | |
| self.assertEqual(0, count.get_active()) | |
| def testDifferentObjects(self): | |
| sequence = Sequence() | |
| def dummy_acquire_fn(): | |
| return None | |
| first_handle = shared.Shared() | |
| second_handle = shared.Shared() | |
| dummy_handle = shared.Shared() | |
| f1 = first_handle.acquire(sequence.make_acquire_fn()) | |
| s1 = second_handle.acquire(sequence.make_acquire_fn()) | |
| self.assertEqual('sequence1', f1.get_name()) | |
| self.assertEqual('sequence2', s1.get_name()) | |
| f2 = first_handle.acquire(sequence.make_acquire_fn()) | |
| s2 = second_handle.acquire(sequence.make_acquire_fn()) | |
| # Check that the repeated acquisitions return the earlier objects | |
| self.assertEqual('sequence1', f2.get_name()) | |
| self.assertEqual('sequence2', s2.get_name()) | |
| # Release all references and force garbage-collection | |
| del f1 | |
| del f2 | |
| del s1 | |
| del s2 | |
| dummy_handle.acquire(dummy_acquire_fn) # Get rid of the keepalive | |
| gc.collect() | |
| # Check that acquiring again after they're released gives new objects | |
| f3 = first_handle.acquire(sequence.make_acquire_fn()) | |
| s3 = second_handle.acquire(sequence.make_acquire_fn()) | |
| self.assertEqual('sequence3', f3.get_name()) | |
| self.assertEqual('sequence4', s3.get_name()) | |
| def testTagCacheEviction(self): | |
| shared1 = shared.Shared() | |
| shared2 = shared.Shared() | |
| def acquire_fn_1(): | |
| return NamedObject('obj_1') | |
| def acquire_fn_2(): | |
| return NamedObject('obj_2') | |
| # with no tag, shared handle does not know when to evict objects | |
| p1 = shared1.acquire(acquire_fn_1) | |
| assert p1.get_name() == 'obj_1' | |
| p2 = shared1.acquire(acquire_fn_2) | |
| assert p2.get_name() == 'obj_1' | |
| # cache eviction can be forced by specifying different tags | |
| p1 = shared2.acquire(acquire_fn_1, tag='1') | |
| assert p1.get_name() == 'obj_1' | |
| p2 = shared2.acquire(acquire_fn_2, tag='2') | |
| assert p2.get_name() == 'obj_2' | |
| def testTagReturnsCached(self): | |
| sequence = Sequence() | |
| handle = shared.Shared() | |
| f1 = handle.acquire(sequence.make_acquire_fn(), tag='1') | |
| self.assertEqual('sequence1', f1.get_name()) | |
| # should return cached | |
| f1 = handle.acquire(sequence.make_acquire_fn(), tag='1') | |
| self.assertEqual('sequence1', f1.get_name()) | |
| if __name__ == '__main__': | |
| unittest.main() |