This isn't "streaming".
Each gigabyte input file will fill up a gigabyte of RAM
before even a single byte of it is sent to the destination server.
With type annotation, the signature would be
def stream_file(source_url: str) -> None:
Or roughly equivalently we might see
def stream_file(source_url: str) -> BytesIO:
with calling sequence of upload(stream_file(source_artifactory_url))
Here is the OP loop:
content_buffer = BytesIO()
for chunk in source_response.iter_content(chunk_size=size):
content_buffer.write(chunk)
streaming
To achieve your stated goal,
you'll want to interleave reads with writes.
uploader = get_uploader(dest_url)
for chunk in source_response.iter_content(chunk_size=size):
content_buffer = BytesIO()
content_buffer.write(chunk)
uploader.send_chunk(content_buffer)
Of course, content_buffer
is no longer serving any useful role
at this point, so we could elide it and simply .send_chunk(chunk)
.
And you'll want to write that get_uploader()
helper.
All of that sets us up for this revised signature:
def stream_file(source_url: str, dest_url: str) -> None:
We're back to evaluating for side effects, returning nothing.
But this time we're streaming! Good.
advantages
network behavior
This "copy chunks from src to dst" loop lets us finish writing
to the destination's disk with lowest possible latency.
You probably care less about that than throughput.
It's also good for throughput, as it lets us keep
the pipe busy in both directions.
Which brings us to the notion of "bottleneck".
With three machines, two filesystems, and two end-to-end TCP connections,
it's unclear whether we run out of CPU cycles somewhere,
or disk I/O bandwidth on {src, dst} filesystem,
or TCP network bandwidth for {down,up}loading.
You will want to make measurements so you better understand that.
Doubling the disk bandwidth won't make any difference
at all if you're routinely maxing out some network link.
The next step on your journey would be to turn this simple
2-connection loop into one which manages 2 × N connections,
so you have N file transfers happening simultaneously.
They could involve the same pair of (very powerful?)
source and destination servers, but could also involve
up to N distinct source and N distinct destination servers.
Which would let you throw lots more hardware resource
at the problem and perhaps finish in an hour rather
than six hours, assuming you've lots of network bandwidth.
memory behavior
Any collection of input files is likely to be comprised
of different sizes, often with a
Zipfian distribution.
If you do a thousand allocate-followed-by-free operations,
of different sizes, your memory footprint keeps ratcheting up
until it hits max( … ) of all those sizes.
Sending each chunk as soon as it arrives reduces local resource needs
down to \$O(1)\$ constant memory complexity, independent of
max size.
disadvantages
If you're going to be transferring file(s) for a few minutes,
then you're sitting on resources on both {src, dst} servers,
which can affect other users of those servers.
Suppose one of the two servers is less powerful and is
therefore the bottleneck.
In the OP non-streaming case the servers are decoupled,
and can finish a {down,up}load task at their maximum speed,
then devote resources to rapidly serving other users.
When streaming, the more powerful one will need to commit
resources for a longer time.
Likely either one of {src, dst} server (with network connection)
will be the bottleneck.
Bumping up the concurrent sessions to 2 or 3 might
increase observed system throughput (byte/second), beyond which
at some point additional sessions won't improve matters.
But the extra overhead from those additional sessions
can impact the response seen by you or by other users.
So "too much concurrency" can be a net lose.
The OP code supports just a single session,
so it is fundamentally unable to produce such lossage.
Take care when tuning the concurrency level,
informed by observed performance figures.
buffer size
That chunk_size=8192
impresses me as being on the small side.
We perform a small 8 KiB chunk of work in the TCP stack,
and then have to wait for cPython to interpret some bytecode,
and then move on to the next small chunk of work.
Better to use bigger chunks, big enough that time spent
interpreting bytecode won't be the dominant source of delay. Use
profiling
measurements to better understand the tradeoffs at different
buffer sizes.