4.4.0
Release highlights
BucketObject.iter_content becomes a real chunked stream on every provider:
it takes a chunk_size, yields chunks sized by that rather than by the
content, and no longer materialises a whole object in memory anywhere.
Separately, two pathological patterns in AWS listing are removed - an image
search that scanned the region's entire public catalogue, and a paginated
call that used the caller's result limit as its transport page size.
Enhancements
iter_contentandsave_contentaccept achunk_size. It
defaults to 1 MiB and is settable globally through theiter_chunk_size
provider config value or theCB_ITER_CHUNK_SIZEenvironment variable,
alongside the existingCB_MULTIPART_*knobs. Previously the read size
was hardcoded and inconsistent - 4 KiB on AWS, 64 KiB on OpenStack Swift,
the service's own chunking on Azure - and callers had no way to change it.
The AWS value dated from the 2017 boto2-to-boto3 migration, where a
BucketObjIteratorshim replaced boto2'sKey(which read in 8 KiB
BufferSizechunks); it was never a tuning decision. Reading an HTTP body
at 1 MiB measures ~12x cheaper per byte than at 4 KiB, and the curve is flat
from 1 MiB up, so larger defaults would only cost memory. Note that
save_contentwas never affected: it copied viashutil.copyfileobj,
which reads in 64 KiB blocks regardless.- New
aws_page_sizeconfiguration value. How many records to request
from AWS per call while satisfying a list method, defaulting to 500. It is
a transport setting, distinct fromdefault_result_limit, which bounds
how many results the caller receives; the two used to be the same number.
It is clamped to what the service permits for the call in hand, so a value
outside those bounds is adjusted rather than rejected. AWS-specific
because it only means anything where the provider walks pages itself:
GCP, Azure and OpenStack each return a single page plus a continuation
token and let the caller drive.
Fixes
- The default network is created with the configured default CIDR.
BaseNetworkService.get_or_create_defaultpassed a hardcoded
10.0.0.0/16instead ofBaseNetwork.CB_DEFAULT_IPV4RANGE, so setting
CB_DEFAULT_IPV4RANGEwas silently ignored on Azure and OpenStack, which
inherit the base implementation. AWS and GCP override it and were already
correct. - Azure no longer splits object content on newlines.
iter_content
returned anio.RawIOBasewrapper, and iterating a raw stream calls
readline()- so chunks broke atb"\n"at whatever sizes the content
happened to dictate, and a blob with no newline in it was buffered whole
however large it was. It now yieldschunk_sizechunks read over a single
connection. - GCP no longer loads the entire object into memory.
iter_content
returnedio.BytesIOwrapped around a fullget_media().execute(), so
streaming a large object cost its full size in RAM (and, being a
BytesIO, also iterated by line). Content is now fetched as successive
ranged reads ofchunk_sizebytes, keeping memory flat at one chunk.
download_to_fileremains the faster path for downloading to disk, as it
fetches ranges in parallel. save_contentno longer requiresiter_contentto return a
file-like object. It copied withshutil.copyfileobj, which needs a
.read()that the interface never promised - onlyIterable[bytes]. It
now writes the iterated chunks directly, so a provider returning a plain
generator works.AWSImageService.findno longer scans every public image to run its
tag search.find(label=...)issues twodescribe_imagescalls, one
filtered onnameand one ontag:Name, and neither was scoped by
Owners. Thetag:Namehalf can only ever match images in the calling
account - AMI tags are not visible across accounts, so an image owned by
anyone else cannot satisfy the filter however it is tagged - so omitting
Ownersnever widened what it could find. It only made EC2 evaluate the
filter against the whole regional catalogue: measured in ap-southeast-1,
10.0s unscoped against 0.1s scoped, for identical single-image results.
Thenamehalf is unchanged and still searches public images, which is
what most callers want; an explicitownersargument still overrides
both.- Paginated AWS calls no longer use the caller's result limit as the
transport page size.BotoEC2Service._get_paginated_resultsset
PaginationConfig={'MaxItems': limit, 'PageSize': limit}, conflating how
many results the caller wants with how many the service returns per
request. Against a scan that matches sparsely that walks the collection in
tiny increments: the same filtereddescribe_imagestook 977.6s at a
page size of 5 and 10.0s at 1000, for one result either way.MaxItems
still bounds what the caller receives;PageSizeis now a full page,
clamped to whatever bounds the service model declares for the operation
(DescribeRouteTablespermits 100 where most permit more, several require
at least 5, and falling outside them is a hardInvalidParameterValue).
SinceDEFAULT_RESULT_LIMITis 50, every paginated AWS call was affected,
not just filtered searches.
Backward compatibility
chunk_size is optional everywhere, so existing calls keep working. The
AWS return value still exposes read/close as before. The Azure and GCP
return values are now plain generators: code that called .read() on them
must iterate instead, or use save_content/download_to_file.
Pull Requests
- Make iter_content a chunked stream with a configurable chunk size by @nuwang in #343
- Create the default network with the configured default CIDR by @nuwang in #344
- Instrument the cloud suites to find where the AWS time goes by @nuwang in #345
- Fix the two effects behind the AWS suite's runtime: unscoped tag search and transport page size by @nuwang in #346
Full Changelog: v4.3.1...v4.4.0