From fc2807cccc773386b83e53d7eb02ee02ef326ba9 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 15 Apr 2022 09:08:16 -0400 Subject: [PATCH 01/19] Sketch of server-side read-test-write endpoint. --- src/allmydata/storage/http_common.py | 1 + src/allmydata/storage/http_server.py | 60 ++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/allmydata/storage/http_common.py b/src/allmydata/storage/http_common.py index addd926d1..123ce403b 100644 --- a/src/allmydata/storage/http_common.py +++ b/src/allmydata/storage/http_common.py @@ -38,6 +38,7 @@ class Secrets(Enum): LEASE_RENEW = "lease-renew-secret" LEASE_CANCEL = "lease-cancel-secret" UPLOAD = "upload-secret" + WRITE_ENABLER = "write-enabler" def get_spki_hash(certificate: Certificate) -> bytes: diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 7c4860d57..bcb4b22c9 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -239,19 +239,39 @@ class _HTTPError(Exception): # https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml. Notably, #6.258 # indicates a set. _SCHEMAS = { - "allocate_buckets": Schema(""" - message = { + "allocate_buckets": Schema( + """ + request = { share-numbers: #6.258([* uint]) allocated-size: uint } - """), - "advise_corrupt_share": Schema(""" - message = { + """ + ), + "advise_corrupt_share": Schema( + """ + request = { reason: tstr } - """) + """ + ), + "mutable_read_test_write": Schema( + """ + request = { + "test-write-vectors": { + * share_number: { + "test": [* {"offset": uint, "size": uint, "specimen": bstr}] + "write": [* {"offset": uint, "data": bstr}] + "new-length": uint + } + } + "read-vector": [* {"offset": uint, "size": uint}] + } + share_number = uint + """ + ), } + class HTTPServer(object): """ A HTTP interface to the storage server. @@ -537,7 +557,9 @@ class HTTPServer(object): "/v1/immutable///corrupt", methods=["POST"], ) - def advise_corrupt_share(self, request, authorization, storage_index, share_number): + def advise_corrupt_share_immutable( + self, request, authorization, storage_index, share_number + ): """Indicate that given share is corrupt, with a text reason.""" try: bucket = self._storage_server.get_buckets(storage_index)[share_number] @@ -548,6 +570,30 @@ class HTTPServer(object): bucket.advise_corrupt_share(info["reason"].encode("utf-8")) return b"" + ##### Immutable APIs ##### + + @_authorized_route( + _app, + {Secrets.LEASE_RENEW, Secrets.LEASE_CANCEL, Secrets.WRITE_ENABLER}, + "/v1/mutable//read-test-write", + methods=["POST"], + ) + def mutable_read_test_write(self, request, authorization, storage_index): + """Read/test/write combined operation for mutables.""" + rtw_request = self._read_encoded(request, _SCHEMAS["mutable_read_test_write"]) + secrets = ( + authorization[Secrets.WRITE_ENABLER], + authorization[Secrets.LEASE_RENEW], + authorization[Secrets.LEASE_CANCEL], + ) + success, read_data = self._storage_server.slot_testv_and_readv_and_writev( + storage_index, + secrets, + rtw_request["test-write-vectors"], + rtw_request["read-vectors"], + ) + return self._send_encoded(request, {"success": success, "data": read_data}) + @implementer(IStreamServerEndpoint) @attr.s From 58bd38120294a6709fc74190188d6ae4ae74a03b Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 15 Apr 2022 09:19:30 -0400 Subject: [PATCH 02/19] Switch to newer attrs API. --- src/allmydata/storage/http_client.py | 42 +++++++++++++--------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 65bcd1c4b..57ca4dae9 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -8,7 +8,7 @@ from typing import Union, Set, Optional from base64 import b64encode -import attr +from attrs import define # TODO Make sure to import Python version? from cbor2 import loads, dumps @@ -121,12 +121,12 @@ def _decode_cbor(response, schema: Schema): ) -@attr.s +@define class ImmutableCreateResult(object): """Result of creating a storage index for an immutable.""" - already_have = attr.ib(type=Set[int]) - allocated = attr.ib(type=Set[int]) + already_have: Set[int] + allocated: Set[int] class _TLSContextFactory(CertificateOptions): @@ -200,14 +200,14 @@ class _TLSContextFactory(CertificateOptions): @implementer(IPolicyForHTTPS) @implementer(IOpenSSLClientConnectionCreator) -@attr.s +@define class _StorageClientHTTPSPolicy: """ A HTTPS policy that ensures the SPKI hash of the public key matches a known hash, i.e. pinning-based validation. """ - expected_spki_hash = attr.ib(type=bytes) + expected_spki_hash: bytes # IPolicyForHTTPS def creatorForNetloc(self, hostname, port): @@ -220,24 +220,22 @@ class _StorageClientHTTPSPolicy: ) +@define class StorageClient(object): """ Low-level HTTP client that talks to the HTTP storage server. """ - def __init__( - self, url, swissnum, treq=treq - ): # type: (DecodedURL, bytes, Union[treq,StubTreq,HTTPClient]) -> None - """ - The URL is a HTTPS URL ("https://..."). To construct from a NURL, use - ``StorageClient.from_nurl()``. - """ - self._base_url = url - self._swissnum = swissnum - self._treq = treq + # The URL is a HTTPS URL ("https://..."). To construct from a NURL, use + # ``StorageClient.from_nurl()``. + _base_url: DecodedURL + _swissnum: bytes + _treq: Union[treq, StubTreq, HTTPClient] @classmethod - def from_nurl(cls, nurl: DecodedURL, reactor, persistent: bool = True) -> StorageClient: + def from_nurl( + cls, nurl: DecodedURL, reactor, persistent: bool = True + ) -> StorageClient: """ Create a ``StorageClient`` for the given NURL. @@ -342,25 +340,25 @@ class StorageClientGeneral(object): returnValue(decoded_response) -@attr.s +@define class UploadProgress(object): """ Progress of immutable upload, per the server. """ # True when upload has finished. - finished = attr.ib(type=bool) + finished: bool # Remaining ranges to upload. - required = attr.ib(type=RangeMap) + required: RangeMap +@define class StorageClientImmutables(object): """ APIs for interacting with immutables. """ - def __init__(self, client: StorageClient): - self._client = client + _client: StorageClient @inlineCallbacks def create( From 186aa9abc4715ef42f7d7d1c7ecd95884bdeab45 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 15 Apr 2022 09:32:15 -0400 Subject: [PATCH 03/19] Make the utility reusable. --- src/allmydata/test/test_deferredutil.py | 28 +++++++++++++++++++ src/allmydata/test/test_storage_https.py | 17 +----------- src/allmydata/util/deferredutil.py | 35 +++++++++++++----------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/allmydata/test/test_deferredutil.py b/src/allmydata/test/test_deferredutil.py index 2a155089f..a37dfdd6f 100644 --- a/src/allmydata/test/test_deferredutil.py +++ b/src/allmydata/test/test_deferredutil.py @@ -129,3 +129,31 @@ class UntilTests(unittest.TestCase): self.assertEqual([1], counter) r1.callback(None) self.assertEqual([2], counter) + + +class AsyncToDeferred(unittest.TestCase): + """Tests for ``deferredutil.async_to_deferred.``""" + + def test_async_to_deferred_success(self): + """ + Normal results from a ``@async_to_deferred``-wrapped function get + turned into a ``Deferred`` with that value. + """ + @deferredutil.async_to_deferred + async def f(x, y): + return x + y + + result = f(1, y=2) + self.assertEqual(self.successResultOf(result), 3) + + def test_async_to_deferred_exception(self): + """ + Exceptions from a ``@async_to_deferred``-wrapped function get + turned into a ``Deferred`` with that value. + """ + @deferredutil.async_to_deferred + async def f(x, y): + return x/y + + result = f(1, 0) + self.assertIsInstance(self.failureResultOf(result).value, ZeroDivisionError) diff --git a/src/allmydata/test/test_storage_https.py b/src/allmydata/test/test_storage_https.py index 73c99725a..3b41e8308 100644 --- a/src/allmydata/test/test_storage_https.py +++ b/src/allmydata/test/test_storage_https.py @@ -6,14 +6,12 @@ server authentication logic, which may one day apply outside of HTTP Storage Protocol. """ -from functools import wraps from contextlib import asynccontextmanager from cryptography import x509 from twisted.internet.endpoints import serverFromString from twisted.internet import reactor -from twisted.internet.defer import Deferred from twisted.internet.task import deferLater from twisted.web.server import Site from twisted.web.static import Data @@ -31,6 +29,7 @@ from .certs import ( from ..storage.http_common import get_spki_hash from ..storage.http_client import _StorageClientHTTPSPolicy from ..storage.http_server import _TLSEndpointWrapper +from ..util.deferredutil import async_to_deferred class HTTPSNurlTests(SyncTestCase): @@ -73,20 +72,6 @@ ox5zO3LrQmQw11OaIAs2/kviKAoKTFFxeyYcpS5RuKNDZfHQCXlLwt9bySxG self.assertEqual(get_spki_hash(certificate), expected_hash) -def async_to_deferred(f): - """ - Wrap an async function to return a Deferred instead. - - Maybe solution to https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3886 - """ - - @wraps(f) - def not_async(*args, **kwargs): - return Deferred.fromCoroutine(f(*args, **kwargs)) - - return not_async - - class PinningHTTPSValidation(AsyncTestCase): """ Test client-side validation logic of HTTPS certificates that uses diff --git a/src/allmydata/util/deferredutil.py b/src/allmydata/util/deferredutil.py index ed2a11ee4..782663e8b 100644 --- a/src/allmydata/util/deferredutil.py +++ b/src/allmydata/util/deferredutil.py @@ -4,24 +4,13 @@ Utilities for working with Twisted Deferreds. Ported to Python 3. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from future.utils import PY2 -if PY2: - from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 - import time +from functools import wraps -try: - from typing import ( - Callable, - Any, - ) -except ImportError: - pass +from typing import ( + Callable, + Any, +) from foolscap.api import eventually from eliot.twisted import ( @@ -231,3 +220,17 @@ def until( yield action() if condition(): break + + +def async_to_deferred(f): + """ + Wrap an async function to return a Deferred instead. + + Maybe solution to https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3886 + """ + + @wraps(f) + def not_async(*args, **kwargs): + return defer.Deferred.fromCoroutine(f(*args, **kwargs)) + + return not_async From 24548dee0b37184cef975d4febe9ca4425920266 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 15 Apr 2022 09:56:06 -0400 Subject: [PATCH 04/19] Sketch of read/write APIs interface for mutables on client side. --- src/allmydata/storage/http_client.py | 134 +++++++++++++++++++++++++-- src/allmydata/storage/http_server.py | 2 +- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 57ca4dae9..1bff34699 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -5,10 +5,10 @@ HTTP client that talks to the HTTP storage server. from __future__ import annotations from typing import Union, Set, Optional - +from enum import Enum from base64 import b64encode -from attrs import define +from attrs import define, field # TODO Make sure to import Python version? from cbor2 import loads, dumps @@ -39,6 +39,7 @@ from .http_common import ( ) from .common import si_b2a from ..util.hashutil import timing_safe_compare +from ..util.deferredutil import async_to_deferred _OPENSSL = Binding().lib @@ -64,7 +65,7 @@ class ClientException(Exception): _SCHEMAS = { "get_version": Schema( """ - message = {'http://allmydata.org/tahoe/protocols/storage/v1' => { + response = {'http://allmydata.org/tahoe/protocols/storage/v1' => { 'maximum-immutable-share-size' => uint 'maximum-mutable-share-size' => uint 'available-space' => uint @@ -79,7 +80,7 @@ _SCHEMAS = { ), "allocate_buckets": Schema( """ - message = { + response = { already-have: #6.258([* uint]) allocated: #6.258([* uint]) } @@ -87,16 +88,25 @@ _SCHEMAS = { ), "immutable_write_share_chunk": Schema( """ - message = { + response = { required: [* {begin: uint, end: uint}] } """ ), "list_shares": Schema( """ - message = #6.258([* uint]) + response = #6.258([* uint]) """ ), + "mutable_read_test_write": Schema( + """ + response = { + "success": bool, + "data": [* share_number: [* bstr]] + } + share_number = uint + """ + ), } @@ -571,3 +581,115 @@ class StorageClientImmutables(object): raise ClientException( response.code, ) + + +@define +class WriteVector: + """Data to write to a chunk.""" + + offset: int + data: bytes + + +class TestVectorOperator(Enum): + """Possible operators for test vectors.""" + + LT = b"lt" + LE = b"le" + EQ = b"eq" + NE = b"ne" + GE = b"ge" + GT = b"gt" + + +@define +class TestVector: + """Checks to make on a chunk before writing to it.""" + + offset: int + size: int + operator: TestVectorOperator = field(default=TestVectorOperator.EQ) + specimen: bytes + + +@define +class ReadVector: + """ + Reads to do on chunks, as part of a read/test/write operation. + """ + + offset: int + size: int + + +@define +class TestWriteVectors: + """Test and write vectors for a specific share.""" + + test_vectors: list[TestVector] + write_vectors: list[WriteVector] + new_length: Optional[int] = field(default=None) + + +@define +class ReadTestWriteResult: + """Result of sending read-test-write vectors.""" + + success: bool + # Map share numbers to reads corresponding to the request's list of + # ReadVectors: + reads: dict[int, list[bytes]] + + +@define +class StorageClientMutables: + """ + APIs for interacting with mutables. + """ + + _client: StorageClient + + @async_to_deferred + async def read_test_write_chunks( + storage_index: bytes, + write_enabled_secret: bytes, + lease_renew_secret: bytes, + lease_cancel_secret: bytes, + testwrite_vectors: dict[int, TestWriteVectors], + read_vector: list[ReadVector], + ) -> ReadTestWriteResult: + """ + Read, test, and possibly write chunks to a particular mutable storage + index. + + Reads are done before writes. + + Given a mapping between share numbers and test/write vectors, the tests + are done and if they are valid the writes are done. + """ + pass + + @async_to_deferred + async def read_share_chunk( + self, + storage_index: bytes, + share_number: int, + # TODO is this really optional? + # TODO if yes, test non-optional variants + offset: Optional[int], + length: Optional[int], + ) -> bytes: + """ + Download a chunk of data from a share. + + TODO https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3857 Failed + downloads should be transparently retried and redownloaded by the + implementation a few times so that if a failure percolates up, the + caller can assume the failure isn't a short-term blip. + + NOTE: the underlying HTTP protocol is much more flexible than this API, + so a future refactor may expand this in order to simplify the calling + code and perhaps download data more efficiently. But then again maybe + the HTTP protocol will be simplified, see + https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3777 + """ diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index bcb4b22c9..7f279580b 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -261,7 +261,7 @@ _SCHEMAS = { * share_number: { "test": [* {"offset": uint, "size": uint, "specimen": bstr}] "write": [* {"offset": uint, "data": bstr}] - "new-length": uint + "new-length": (uint // null) } } "read-vector": [* {"offset": uint, "size": uint}] From b0d547ee53540649e18ceb437b7508d22a12dbaa Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 18 Apr 2022 14:56:20 -0400 Subject: [PATCH 05/19] Progress on implementing client side of mutable writes. --- src/allmydata/storage/http_client.py | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 1bff34699..8899614b8 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -8,7 +8,7 @@ from typing import Union, Set, Optional from enum import Enum from base64 import b64encode -from attrs import define, field +from attrs import define, field, asdict # TODO Make sure to import Python version? from cbor2 import loads, dumps @@ -288,6 +288,7 @@ class StorageClient(object): lease_renew_secret=None, lease_cancel_secret=None, upload_secret=None, + write_enabler_secret=None, headers=None, message_to_serialize=None, **kwargs @@ -306,6 +307,7 @@ class StorageClient(object): (Secrets.LEASE_RENEW, lease_renew_secret), (Secrets.LEASE_CANCEL, lease_cancel_secret), (Secrets.UPLOAD, upload_secret), + (Secrets.WRITE_ENABLER, write_enabler_secret), ]: if value is None: continue @@ -651,8 +653,9 @@ class StorageClientMutables: @async_to_deferred async def read_test_write_chunks( + self, storage_index: bytes, - write_enabled_secret: bytes, + write_enabler_secret: bytes, lease_renew_secret: bytes, lease_cancel_secret: bytes, testwrite_vectors: dict[int, TestWriteVectors], @@ -667,7 +670,31 @@ class StorageClientMutables: Given a mapping between share numbers and test/write vectors, the tests are done and if they are valid the writes are done. """ - pass + # TODO unit test all the things + url = self._client.relative_url( + "/v1/mutable/{}/read-test-write".format(_encode_si(storage_index)) + ) + message = { + "test-write-vectors": { + share_number: asdict(twv) + for (share_number, twv) in testwrite_vectors.items() + }, + "read-vector": [asdict(r) for r in read_vector], + } + response = yield self._client.request( + "POST", + url, + write_enabler_secret=write_enabler_secret, + lease_renew_secret=lease_renew_secret, + lease_cancel_secret=lease_cancel_secret, + message_to_serialize=message, + ) + if response.code == http.OK: + return _decode_cbor(response, _SCHEMAS["mutable_test_read_write"]) + else: + raise ClientException( + response.code, + ) @async_to_deferred async def read_share_chunk( From 2ca5e22af9788aa4fccd3b25ed5c3188ef049ecb Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 11:47:22 -0400 Subject: [PATCH 06/19] Make mutable reading match immutable reading. --- docs/proposed/http-storage-node-protocol.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/proposed/http-storage-node-protocol.rst b/docs/proposed/http-storage-node-protocol.rst index 9354bc185..3926d9f4a 100644 --- a/docs/proposed/http-storage-node-protocol.rst +++ b/docs/proposed/http-storage-node-protocol.rst @@ -743,11 +743,15 @@ For example:: [1, 5] -``GET /v1/mutable/:storage_index?share=:s0&share=:sN&offset=:o1&size=:z0&offset=:oN&size=:zN`` +``GET /v1/mutable/:storage_index/:share_number`` !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -Read data from the indicated mutable shares. -Just like ``GET /v1/mutable/:storage_index``. +Read data from the indicated mutable shares, just like ``GET /v1/immutable/:storage_index`` + +The ``Range`` header may be used to request exactly one ``bytes`` range, in which case the response code will be 206 (partial content). +Interpretation and response behavior is as specified in RFC 7233 ยง 4.1. +Multiple ranges in a single request are *not* supported; open-ended ranges are also not supported. + ``POST /v1/mutable/:storage_index/:share_number/corrupt`` !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! From 898fe0bc0e48489668cf58981a18a252c9ed587f Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 13:18:31 -0400 Subject: [PATCH 07/19] Closer to running end-to-end mutable tests. --- src/allmydata/storage/http_client.py | 107 ++++++++++++---------- src/allmydata/storage/http_server.py | 4 +- src/allmydata/storage_client.py | 54 ++++++++++- src/allmydata/test/test_istorageserver.py | 8 +- 4 files changed, 122 insertions(+), 51 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 8899614b8..52177f401 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -364,6 +364,46 @@ class UploadProgress(object): required: RangeMap +@inlineCallbacks +def read_share_chunk( + client: StorageClient, + share_type: str, + storage_index: bytes, + share_number: int, + offset: int, + length: int, +) -> Deferred[bytes]: + """ + Download a chunk of data from a share. + + TODO https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3857 Failed + downloads should be transparently retried and redownloaded by the + implementation a few times so that if a failure percolates up, the + caller can assume the failure isn't a short-term blip. + + NOTE: the underlying HTTP protocol is much more flexible than this API, + so a future refactor may expand this in order to simplify the calling + code and perhaps download data more efficiently. But then again maybe + the HTTP protocol will be simplified, see + https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3777 + """ + url = client.relative_url( + "/v1/{}/{}/{}".format(share_type, _encode_si(storage_index), share_number) + ) + response = yield client.request( + "GET", + url, + headers=Headers( + {"range": [Range("bytes", [(offset, offset + length)]).to_header()]} + ), + ) + if response.code == http.PARTIAL_CONTENT: + body = yield response.content() + returnValue(body) + else: + raise ClientException(response.code) + + @define class StorageClientImmutables(object): """ @@ -484,39 +524,15 @@ class StorageClientImmutables(object): remaining.set(True, chunk["begin"], chunk["end"]) returnValue(UploadProgress(finished=finished, required=remaining)) - @inlineCallbacks def read_share_chunk( self, storage_index, share_number, offset, length ): # type: (bytes, int, int, int) -> Deferred[bytes] """ Download a chunk of data from a share. - - TODO https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3857 Failed - downloads should be transparently retried and redownloaded by the - implementation a few times so that if a failure percolates up, the - caller can assume the failure isn't a short-term blip. - - NOTE: the underlying HTTP protocol is much more flexible than this API, - so a future refactor may expand this in order to simplify the calling - code and perhaps download data more efficiently. But then again maybe - the HTTP protocol will be simplified, see - https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3777 """ - url = self._client.relative_url( - "/v1/immutable/{}/{}".format(_encode_si(storage_index), share_number) + return read_share_chunk( + self._client, "immutable", storage_index, share_number, offset, length ) - response = yield self._client.request( - "GET", - url, - headers=Headers( - {"range": [Range("bytes", [(offset, offset + length)]).to_header()]} - ), - ) - if response.code == http.PARTIAL_CONTENT: - body = yield response.content() - returnValue(body) - else: - raise ClientException(response.code) @inlineCallbacks def list_shares(self, storage_index): # type: (bytes,) -> Deferred[Set[int]] @@ -610,7 +626,7 @@ class TestVector: offset: int size: int - operator: TestVectorOperator = field(default=TestVectorOperator.EQ) + operator: TestVectorOperator specimen: bytes @@ -632,6 +648,14 @@ class TestWriteVectors: write_vectors: list[WriteVector] new_length: Optional[int] = field(default=None) + def asdict(self) -> dict: + """Return dictionary suitable for sending over CBOR.""" + d = asdict(self) + d["test"] = d.pop("test_vectors") + d["write"] = d.pop("write_vectors") + d["new-length"] = d.pop("new_length") + return d + @define class ReadTestWriteResult: @@ -676,12 +700,12 @@ class StorageClientMutables: ) message = { "test-write-vectors": { - share_number: asdict(twv) + share_number: twv.asdict() for (share_number, twv) in testwrite_vectors.items() }, "read-vector": [asdict(r) for r in read_vector], } - response = yield self._client.request( + response = await self._client.request( "POST", url, write_enabler_secret=write_enabler_secret, @@ -692,31 +716,20 @@ class StorageClientMutables: if response.code == http.OK: return _decode_cbor(response, _SCHEMAS["mutable_test_read_write"]) else: - raise ClientException( - response.code, - ) + raise ClientException(response.code, (await response.content())) @async_to_deferred async def read_share_chunk( self, storage_index: bytes, share_number: int, - # TODO is this really optional? - # TODO if yes, test non-optional variants - offset: Optional[int], - length: Optional[int], + offset: int, + length: int, ) -> bytes: """ Download a chunk of data from a share. - - TODO https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3857 Failed - downloads should be transparently retried and redownloaded by the - implementation a few times so that if a failure percolates up, the - caller can assume the failure isn't a short-term blip. - - NOTE: the underlying HTTP protocol is much more flexible than this API, - so a future refactor may expand this in order to simplify the calling - code and perhaps download data more efficiently. But then again maybe - the HTTP protocol will be simplified, see - https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3777 """ + # TODO unit test all the things + return read_share_chunk( + self._client, "mutable", storage_index, share_number, offset, length + ) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 7f279580b..6def5aeeb 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -261,7 +261,7 @@ _SCHEMAS = { * share_number: { "test": [* {"offset": uint, "size": uint, "specimen": bstr}] "write": [* {"offset": uint, "data": bstr}] - "new-length": (uint // null) + "new-length": uint // null } } "read-vector": [* {"offset": uint, "size": uint}] @@ -590,7 +590,7 @@ class HTTPServer(object): storage_index, secrets, rtw_request["test-write-vectors"], - rtw_request["read-vectors"], + rtw_request["read-vector"], ) return self._send_encoded(request, {"success": success, "data": read_data}) diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 55b6cfb05..afed0e274 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -77,7 +77,8 @@ from allmydata.util.hashutil import permute_server_hash from allmydata.util.dictutil import BytesKeyDict, UnicodeKeyDict from allmydata.storage.http_client import ( StorageClient, StorageClientImmutables, StorageClientGeneral, - ClientException as HTTPClientException + ClientException as HTTPClientException, StorageClientMutables, + ReadVector, TestWriteVectors, WriteVector, TestVector, TestVectorOperator ) @@ -1189,3 +1190,54 @@ class _HTTPStorageServer(object): ) else: raise NotImplementedError() # future tickets + + @defer.inlineCallbacks + def slot_readv(self, storage_index, shares, readv): + mutable_client = StorageClientMutables(self._http_client) + reads = {} + for share_number in shares: + share_reads = reads[share_number] = [] + for (offset, length) in readv: + d = mutable_client.read_share_chunk( + storage_index, share_number, offset, length + ) + share_reads.append(d) + result = { + share_number: [(yield d) for d in share_reads] + for (share_number, reads) in reads.items() + } + defer.returnValue(result) + + @defer.inlineCallbacks + def slot_testv_and_readv_and_writev( + self, + storage_index, + secrets, + tw_vectors, + r_vector, + ): + mutable_client = StorageClientMutables(self._http_client) + we_secret, lr_secret, lc_secret = secrets + client_tw_vectors = {} + for share_num, (test_vector, data_vector, new_length) in tw_vectors.items(): + client_test_vectors = [ + TestVector(offset=offset, size=size, operator=TestVectorOperator[op], specimen=specimen) + for (offset, size, op, specimen) in test_vector + ] + client_write_vectors = [ + WriteVector(offset=offset, data=data) for (offset, data) in data_vector + ] + client_tw_vectors[share_num] = TestWriteVectors( + test_vectors=client_test_vectors, + write_vectors=client_write_vectors, + new_length=new_length + ) + client_read_vectors = [ + ReadVector(offset=offset, size=size) + for (offset, size) in r_vector + ] + client_result = yield mutable_client.read_test_write_chunks( + storage_index, we_secret, lr_secret, lc_secret, client_tw_vectors, + client_read_vectors, + ) + defer.returnValue((client_result.success, client_result.reads)) diff --git a/src/allmydata/test/test_istorageserver.py b/src/allmydata/test/test_istorageserver.py index 3d6f610be..702c66952 100644 --- a/src/allmydata/test/test_istorageserver.py +++ b/src/allmydata/test/test_istorageserver.py @@ -1140,4 +1140,10 @@ class HTTPImmutableAPIsTests( class FoolscapMutableAPIsTests( _FoolscapMixin, IStorageServerMutableAPIsTestsMixin, AsyncTestCase ): - """Foolscap-specific tests for immutable ``IStorageServer`` APIs.""" + """Foolscap-specific tests for mutable ``IStorageServer`` APIs.""" + + +class HTTPMutableAPIsTests( + _HTTPMixin, IStorageServerMutableAPIsTestsMixin, AsyncTestCase +): + """HTTP-specific tests for mutable ``IStorageServer`` APIs.""" From f5c4513cd38c79ec2af2fdba18811f023134c42b Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 13:35:09 -0400 Subject: [PATCH 08/19] A little closer to serialization and deserialization working correctly, with some tests passing. --- src/allmydata/storage/http_client.py | 20 ++++---------------- src/allmydata/storage/http_server.py | 11 +++++++++-- src/allmydata/storage_client.py | 6 +++--- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 52177f401..7b80ec602 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -102,7 +102,7 @@ _SCHEMAS = { """ response = { "success": bool, - "data": [* share_number: [* bstr]] + "data": {* share_number: [* bstr]} } share_number = uint """ @@ -609,24 +609,12 @@ class WriteVector: data: bytes -class TestVectorOperator(Enum): - """Possible operators for test vectors.""" - - LT = b"lt" - LE = b"le" - EQ = b"eq" - NE = b"ne" - GE = b"ge" - GT = b"gt" - - @define class TestVector: """Checks to make on a chunk before writing to it.""" offset: int size: int - operator: TestVectorOperator specimen: bytes @@ -714,12 +702,12 @@ class StorageClientMutables: message_to_serialize=message, ) if response.code == http.OK: - return _decode_cbor(response, _SCHEMAS["mutable_test_read_write"]) + result = await _decode_cbor(response, _SCHEMAS["mutable_read_test_write"]) + return ReadTestWriteResult(success=result["success"], reads=result["data"]) else: raise ClientException(response.code, (await response.content())) - @async_to_deferred - async def read_share_chunk( + def read_share_chunk( self, storage_index: bytes, share_number: int, diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 6def5aeeb..3eae476b7 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -589,8 +589,15 @@ class HTTPServer(object): success, read_data = self._storage_server.slot_testv_and_readv_and_writev( storage_index, secrets, - rtw_request["test-write-vectors"], - rtw_request["read-vector"], + { + k: ( + [(d["offset"], d["size"], b"eq", d["specimen"]) for d in v["test"]], + [(d["offset"], d["data"]) for d in v["write"]], + v["new-length"], + ) + for (k, v) in rtw_request["test-write-vectors"].items() + }, + [(d["offset"], d["size"]) for d in rtw_request["read-vector"]], ) return self._send_encoded(request, {"success": success, "data": read_data}) diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index afed0e274..5321efb7d 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -78,7 +78,7 @@ from allmydata.util.dictutil import BytesKeyDict, UnicodeKeyDict from allmydata.storage.http_client import ( StorageClient, StorageClientImmutables, StorageClientGeneral, ClientException as HTTPClientException, StorageClientMutables, - ReadVector, TestWriteVectors, WriteVector, TestVector, TestVectorOperator + ReadVector, TestWriteVectors, WriteVector, TestVector ) @@ -1221,8 +1221,8 @@ class _HTTPStorageServer(object): client_tw_vectors = {} for share_num, (test_vector, data_vector, new_length) in tw_vectors.items(): client_test_vectors = [ - TestVector(offset=offset, size=size, operator=TestVectorOperator[op], specimen=specimen) - for (offset, size, op, specimen) in test_vector + TestVector(offset=offset, size=size, specimen=specimen) + for (offset, size, specimen) in test_vector ] client_write_vectors = [ WriteVector(offset=offset, data=data) for (offset, data) in data_vector From 21c3c50e37114a7a3e7e6d02ebe08af1101c45f3 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 15:07:57 -0400 Subject: [PATCH 09/19] Basic mutable read support. --- src/allmydata/storage/http_server.py | 43 ++++++++++++++++++++++++++++ src/allmydata/storage_client.py | 13 ++++----- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 3eae476b7..dbb79cf2b 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -601,6 +601,49 @@ class HTTPServer(object): ) return self._send_encoded(request, {"success": success, "data": read_data}) + @_authorized_route( + _app, + set(), + "/v1/mutable//", + methods=["GET"], + ) + def read_mutable_chunk(self, request, authorization, storage_index, share_number): + """Read a chunk from a mutable.""" + if request.getHeader("range") is None: + # TODO in follow-up ticket + raise NotImplementedError() + + # TODO reduce duplication with immutable reads? + # TODO unit tests, perhaps shared if possible + range_header = parse_range_header(request.getHeader("range")) + if ( + range_header is None + or range_header.units != "bytes" + or len(range_header.ranges) > 1 # more than one range + or range_header.ranges[0][1] is None # range without end + ): + request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE) + return b"" + + offset, end = range_header.ranges[0] + + # TODO limit memory usage + # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3872 + data = self._storage_server.slot_readv( + storage_index, [share_number], [(offset, end - offset)] + )[share_number][0] + + # TODO reduce duplication? + request.setResponseCode(http.PARTIAL_CONTENT) + if len(data): + # For empty bodies the content-range header makes no sense since + # the end of the range is inclusive. + request.setHeader( + "content-range", + ContentRange("bytes", offset, offset + len(data)).to_header(), + ) + return data + @implementer(IStreamServerEndpoint) @attr.s diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 5321efb7d..857b19ed7 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -1195,18 +1195,17 @@ class _HTTPStorageServer(object): def slot_readv(self, storage_index, shares, readv): mutable_client = StorageClientMutables(self._http_client) reads = {} + # TODO if shares list is empty, that means list all shares, so we need + # to do a query to get that. + assert shares # TODO replace with call to list shares for share_number in shares: share_reads = reads[share_number] = [] for (offset, length) in readv: - d = mutable_client.read_share_chunk( + r = yield mutable_client.read_share_chunk( storage_index, share_number, offset, length ) - share_reads.append(d) - result = { - share_number: [(yield d) for d in share_reads] - for (share_number, reads) in reads.items() - } - defer.returnValue(result) + share_reads.append(r) + defer.returnValue(reads) @defer.inlineCallbacks def slot_testv_and_readv_and_writev( From f03feb0595c63c5cfbcd72cfa1243d9e7e375fe4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 15:08:07 -0400 Subject: [PATCH 10/19] TODOs for later. --- src/allmydata/storage/http_server.py | 1 + src/allmydata/test/test_istorageserver.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index dbb79cf2b..b71877bbf 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -580,6 +580,7 @@ class HTTPServer(object): ) def mutable_read_test_write(self, request, authorization, storage_index): """Read/test/write combined operation for mutables.""" + # TODO unit tests rtw_request = self._read_encoded(request, _SCHEMAS["mutable_read_test_write"]) secrets = ( authorization[Secrets.WRITE_ENABLER], diff --git a/src/allmydata/test/test_istorageserver.py b/src/allmydata/test/test_istorageserver.py index 702c66952..e7b869713 100644 --- a/src/allmydata/test/test_istorageserver.py +++ b/src/allmydata/test/test_istorageserver.py @@ -1147,3 +1147,12 @@ class HTTPMutableAPIsTests( _HTTPMixin, IStorageServerMutableAPIsTestsMixin, AsyncTestCase ): """HTTP-specific tests for mutable ``IStorageServer`` APIs.""" + + # TODO will be implemented in later tickets + SKIP_TESTS = { + "test_STARAW_write_enabler_must_match", + "test_add_lease_renewal", + "test_add_new_lease", + "test_advise_corrupt_share", + "test_slot_readv_no_shares", + } From 5ccafb2a032cdb5a91abd12be6fb0756465711f4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 15:08:30 -0400 Subject: [PATCH 11/19] News file. --- newsfragments/3890.mi | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3890.mi diff --git a/newsfragments/3890.mi b/newsfragments/3890.mi new file mode 100644 index 000000000..e69de29bb From 72c59b5f1a9a2960e53379d3753c0ee1fd5b5de3 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 19 Apr 2022 15:09:02 -0400 Subject: [PATCH 12/19] Unused import. --- src/allmydata/storage/http_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 7b80ec602..09aada555 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -5,7 +5,6 @@ HTTP client that talks to the HTTP storage server. from __future__ import annotations from typing import Union, Set, Optional -from enum import Enum from base64 import b64encode from attrs import define, field, asdict From 3d710406ef0b222f29af023acc7dedcb156f41f5 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 26 Apr 2022 15:50:23 -0400 Subject: [PATCH 13/19] News file. --- newsfragments/3890.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3890.minor diff --git a/newsfragments/3890.minor b/newsfragments/3890.minor new file mode 100644 index 000000000..e69de29bb From 49c16f2a1acf804a6cd2c9e66ab46e10c888d463 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 27 Apr 2022 08:38:22 -0400 Subject: [PATCH 14/19] Delete 3890.mi remove spurious news fragment --- newsfragments/3890.mi | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 newsfragments/3890.mi diff --git a/newsfragments/3890.mi b/newsfragments/3890.mi deleted file mode 100644 index e69de29bb..000000000 From e16eb6dddfcb46dc530ba5ad81c4710578b6c609 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 28 Apr 2022 11:46:37 -0400 Subject: [PATCH 15/19] Better type definitions. --- src/allmydata/storage/http_client.py | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 09aada555..da350e0c6 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -4,10 +4,10 @@ HTTP client that talks to the HTTP storage server. from __future__ import annotations -from typing import Union, Set, Optional +from typing import Union, Optional, Sequence, Mapping from base64 import b64encode -from attrs import define, field, asdict +from attrs import define, asdict, frozen # TODO Make sure to import Python version? from cbor2 import loads, dumps @@ -134,8 +134,8 @@ def _decode_cbor(response, schema: Schema): class ImmutableCreateResult(object): """Result of creating a storage index for an immutable.""" - already_have: Set[int] - allocated: Set[int] + already_have: set[int] + allocated: set[int] class _TLSContextFactory(CertificateOptions): @@ -420,7 +420,7 @@ class StorageClientImmutables(object): upload_secret, lease_renew_secret, lease_cancel_secret, - ): # type: (bytes, Set[int], int, bytes, bytes, bytes) -> Deferred[ImmutableCreateResult] + ): # type: (bytes, set[int], int, bytes, bytes, bytes) -> Deferred[ImmutableCreateResult] """ Create a new storage index for an immutable. @@ -534,7 +534,7 @@ class StorageClientImmutables(object): ) @inlineCallbacks - def list_shares(self, storage_index): # type: (bytes,) -> Deferred[Set[int]] + def list_shares(self, storage_index): # type: (bytes,) -> Deferred[set[int]] """ Return the set of shares for a given storage index. """ @@ -600,7 +600,7 @@ class StorageClientImmutables(object): ) -@define +@frozen class WriteVector: """Data to write to a chunk.""" @@ -608,7 +608,7 @@ class WriteVector: data: bytes -@define +@frozen class TestVector: """Checks to make on a chunk before writing to it.""" @@ -617,7 +617,7 @@ class TestVector: specimen: bytes -@define +@frozen class ReadVector: """ Reads to do on chunks, as part of a read/test/write operation. @@ -627,13 +627,13 @@ class ReadVector: size: int -@define +@frozen class TestWriteVectors: """Test and write vectors for a specific share.""" - test_vectors: list[TestVector] - write_vectors: list[WriteVector] - new_length: Optional[int] = field(default=None) + test_vectors: Sequence[TestVector] + write_vectors: Sequence[WriteVector] + new_length: Optional[int] = None def asdict(self) -> dict: """Return dictionary suitable for sending over CBOR.""" @@ -644,17 +644,17 @@ class TestWriteVectors: return d -@define +@frozen class ReadTestWriteResult: """Result of sending read-test-write vectors.""" success: bool # Map share numbers to reads corresponding to the request's list of # ReadVectors: - reads: dict[int, list[bytes]] + reads: Mapping[int, Sequence[bytes]] -@define +@frozen class StorageClientMutables: """ APIs for interacting with mutables. From 76d0cfb770f5f886889a0d5731a6fdde9ab3665e Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 28 Apr 2022 11:49:21 -0400 Subject: [PATCH 16/19] Correct comment. --- src/allmydata/storage/http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index b71877bbf..0169d1463 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -570,7 +570,7 @@ class HTTPServer(object): bucket.advise_corrupt_share(info["reason"].encode("utf-8")) return b"" - ##### Immutable APIs ##### + ##### Mutable APIs ##### @_authorized_route( _app, From b8b1d7515a392984ee34c0d9af8d693fffa9089b Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 28 Apr 2022 11:59:50 -0400 Subject: [PATCH 17/19] We can at least be efficient when possible. --- src/allmydata/storage_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 857b19ed7..9c6d5faa2 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -1197,7 +1197,7 @@ class _HTTPStorageServer(object): reads = {} # TODO if shares list is empty, that means list all shares, so we need # to do a query to get that. - assert shares # TODO replace with call to list shares + assert shares # TODO replace with call to list shares if and only if it's empty for share_number in shares: share_reads = reads[share_number] = [] for (offset, length) in readv: From 5ce204ed8d514eeaf5344f5ec3a774311137702a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 28 Apr 2022 12:18:58 -0400 Subject: [PATCH 18/19] Make queries run in parallel. --- src/allmydata/storage_client.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 9c6d5faa2..68164e697 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -1194,18 +1194,29 @@ class _HTTPStorageServer(object): @defer.inlineCallbacks def slot_readv(self, storage_index, shares, readv): mutable_client = StorageClientMutables(self._http_client) + pending_reads = {} reads = {} # TODO if shares list is empty, that means list all shares, so we need # to do a query to get that. assert shares # TODO replace with call to list shares if and only if it's empty + + # Start all the queries in parallel: for share_number in shares: - share_reads = reads[share_number] = [] - for (offset, length) in readv: - r = yield mutable_client.read_share_chunk( - storage_index, share_number, offset, length - ) - share_reads.append(r) - defer.returnValue(reads) + share_reads = defer.gatherResults( + [ + mutable_client.read_share_chunk( + storage_index, share_number, offset, length + ) + for (offset, length) in readv + ] + ) + pending_reads[share_number] = share_reads + + # Wait for all the queries to finish: + for share_number, pending_result in pending_reads.items(): + reads[share_number] = yield pending_result + + return reads @defer.inlineCallbacks def slot_testv_and_readv_and_writev( @@ -1239,4 +1250,4 @@ class _HTTPStorageServer(object): storage_index, we_secret, lr_secret, lc_secret, client_tw_vectors, client_read_vectors, ) - defer.returnValue((client_result.success, client_result.reads)) + return (client_result.success, client_result.reads) From 36e3beaa482df538eed024162c0302a71af24751 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 29 Apr 2022 10:03:43 -0400 Subject: [PATCH 19/19] Get rid of deprecations builder. --- .circleci/config.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cf0c66aff..79ce57ed0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,8 +48,6 @@ workflows: {} - "pyinstaller": {} - - "deprecations": - {} - "c-locale": {} # Any locale other than C or UTF-8. @@ -297,20 +295,6 @@ jobs: # aka "Latin 1" LANG: "en_US.ISO-8859-1" - - deprecations: - <<: *DEBIAN - - environment: - <<: *UTF_8_ENVIRONMENT - # Select the deprecations tox environments. - TAHOE_LAFS_TOX_ENVIRONMENT: "deprecations,upcoming-deprecations" - # Put the logs somewhere we can report them. - TAHOE_LAFS_WARNINGS_LOG: "/tmp/artifacts/deprecation-warnings.log" - # The deprecations tox environments don't do coverage measurement. - UPLOAD_COVERAGE: "" - - integration: <<: *DEBIAN