Skip to content
Snippets Groups Projects
test_lease_maintenance.py 8.73 KiB
Newer Older
  • Learn to ignore specific revisions
  • # Copyright 2019 PrivateStorage.io, LLC
    #
    # Licensed 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.
    
    """
    Tests for ``_zkapauthorizer.lease_maintenance``.
    """
    
    from __future__ import (
        absolute_import,
        unicode_literals,
    )
    
    from datetime import (
    
        timedelta,
    )
    
    import attr
    
    from testtools import (
        TestCase,
    )
    
    from testtools.matchers import (
        Equals,
    )
    
    from testtools.twistedsupport import (
        succeeds,
    )
    
    from hypothesis import (
        given,
    
    from hypothesis.strategies import (
        builds,
        binary,
        integers,
        lists,
    
        dictionaries,
    
    )
    
    from twisted.internet.task import (
        Clock,
    )
    from twisted.internet.defer import (
        succeed,
    )
    from twisted.application.service import (
        IService,
    )
    
    from allmydata.util.hashutil import (
        CRYPTO_VAL_SIZE,
    )
    
    from allmydata.client import (
        SecretHolder,
    )
    
    
    from ..foolscap import (
        ShareStat,
    )
    
    from .matchers import (
        Provides,
    
    )
    from .strategies import (
        storage_indexes,
    
    from ..lease_maintenance import (
    
        lease_maintenance_service,
    
    def interval_means():
        return floats(
            # It doesn't make sense to have a negative check interval mean.
            min_value=0,
            # We can't make this value too large or it isn't convertable to a
            # timedelta.  Also, even values as large as this one are of
            # questionable value.
            max_value=60 * 60 * 24 * 365,
        ).map(
            # By representing the result as a timedelta we avoid the cases where
            # the lower precision of timedelta compared to float drops the whole
            # value (anything between 0 and 1 microsecond).  This is just on
            # example of how working with timedeltas is nicer, in general.
            lambda s: timedelta(seconds=s),
        )
    
    
    
    def dummy_maintain_leases():
        pass
    
    
    
    @attr.s
    class DummyStorageServer(object):
        """
        :ivar dict[bytes, datetime] buckets: A mapping from storage index to lease
            expiration time for shares at that storage index.
        """
        clock = attr.ib()
        buckets = attr.ib()
        lease_seed = attr.ib()
    
        def stat_shares(self, storage_indexes):
            return succeed(list(
                self.buckets[idx]
                for idx
                in storage_indexes
            ))
    
        def get_lease_seed(self):
            return self.lease_seed
    
        def renew_lease(self, storage_index, renew_secret):
            self.buckets[storage_index].lease_expiration = (
                self.clock.seconds() + timedelta(days=31).total_seconds()
            )
    
    
    def lease_seeds():
        return binary(
            min_size=CRYPTO_VAL_SIZE,
            max_size=CRYPTO_VAL_SIZE,
        )
    
    def share_stats():
        return builds(
            ShareStat,
            size=integers(min_value=0),
            lease_expiration=integers(min_value=0, max_value=2 ** 31),
        )
    
    def storage_servers(clocks):
        return builds(
            DummyStorageServer,
            clocks,
            dictionaries(storage_indexes(), share_stats()),
            lease_seeds(),
        )
    
    
    @attr.s
    class DummyStorageBroker(object):
        clock = attr.ib()
        _storage_servers = attr.ib()
    
        def get_connected_servers(self):
            return self._storage_servers
    
    
    def storage_brokers(clocks):
        return builds(
            DummyStorageBroker,
            lists(storage_servers(clocks)),
        )
    
    
    class LeaseMaintenanceServiceTests(TestCase):
        """
        Tests for the service returned by ``lease_maintenance_service``.
        """
    
        @given(randoms())
        def test_interface(self, random):
    
            """
            The service provides ``IService``.
            """
            clock = Clock()
            service = lease_maintenance_service(
    
                dummy_maintain_leases,
    
                random,
            )
            self.assertThat(
                service,
    
        )
        def test_initial_interval(self, random, mean):
            """
            When constructed without a value for ``last_run``,
            ``lease_maintenance_service`` schedules its first run to take place
            after an interval that falls uniformly in range centered on ``mean``
            with a size of ``range``.
            """
            clock = Clock()
            # Construct a range that fits in with the mean
    
            range_ = timedelta(
                seconds=random.uniform(0, mean.total_seconds()),
            )
    
    
            service = lease_maintenance_service(
    
                dummy_maintain_leases,
    
            [maintenance_call] = clock.getDelayedCalls()
    
            datetime_now = datetime.utcfromtimestamp(clock.seconds())
            low = datetime_now + mean - (range_ / 2)
            high = datetime_now + mean + (range_ / 2)
            self.assertThat(
                datetime.utcfromtimestamp(maintenance_call.getTime()),
                between(low, high),
            )
    
        @given(
            randoms(),
            clocks(),
            interval_means(),
            interval_means(),
        )
        def test_initial_interval_with_last_run(self, random, clock, mean, since_last_run):
            """
            When constructed with a value for ``last_run``,
            ``lease_maintenance_service`` schedules its first run to take place
            sooner than it otherwise would, by at most the time since the last
            run.
            """
            datetime_now = datetime.utcfromtimestamp(clock.seconds())
            # Construct a range that fits in with the mean
            range_ = timedelta(
                seconds=random.uniform(0, mean.total_seconds()),
            )
    
            # Figure out the absolute last run time.
            last_run = datetime_now - since_last_run
    
            service = lease_maintenance_service(
    
                dummy_maintain_leases,
    
                clock,
                last_run,
                random,
                mean,
                range_,
            )
            service.startService()
    
            [maintenance_call] = clock.getDelayedCalls()
    
    
            low = datetime_now + max(
                timedelta(0),
                mean - (range_ / 2) - since_last_run,
            )
            high = max(
                # If since_last_run is one microsecond (precision of timedelta)
                # then the range is indivisible.  Avoid putting the expected high
                # below the expected low.
                low,
                datetime_now + mean + (range_ / 2) - since_last_run,
            )
    
            note("mean: {}\nrange: {}\nnow: {}\nlow: {}\nhigh: {}\nsince last: {}".format(
                mean, range_, datetime_now, low, high, since_last_run,
            ))
    
    
                datetime.utcfromtimestamp(maintenance_call.getTime()),
                between(low, high),
    
    
        @given(
            randoms(),
            clocks(),
        )
        def test_nodes_visited(self, random, clock):
            """
            When the service runs, it calls the ``maintain_leases`` object.
            """
            leases_maintained_at = []
            def maintain_leases():
                leases_maintained_at.append(datetime.utcfromtimestamp(clock.seconds()))
    
            service = lease_maintenance_service(
                maintain_leases,
                clock,
                None,
                random,
            )
            service.startService()
            [maintenance_call] = clock.getDelayedCalls()
            clock.advance(maintenance_call.getTime() - clock.seconds())
    
            self.assertThat(
                leases_maintained_at,
                Equals([datetime.utcfromtimestamp(clock.seconds())]),
            )
    
    
    
    class MaintainLeasesFromRootTests(TestCase):
        """
        Tests for ``maintain_leases_from_root``.
        """
        @given(node_hierarchies(), clocks())
        def test_visits_all_nodes(self, root_node, clock):
            """
            The operation calls the specified visitor with every node from the root to
            its deepest children.
            """
            visited = []
            def visitor(node):
                visited.append(node)
    
            storage_broker = DummyStorageBroker(clock, [])
            secret_holder = SecretHolder(lease_secret, convergence_secret)
    
            operation = maintain_leases_from_root(
                visitor,
                root_node,
                storage_broker,
                secret_holder,
                timedelta(days=3),
                lambda: datetime.utcfromtimestamp(clock.seconds()),
            )
    
            self.assertThat(
                operation(root_node),
                succeeds(Always()),
            )