Writing a download server. Part V: Throttle download speed
I must admit, I love
java.io
design with lots of simple Input
/OutputStream
and Reader
/Writer
implementations, each having just one responsibility. You want buffering? GZIPing? Character encoding? File system writing? Just compose desired classes that always work with each other. All right, it's still blocking, but it was designed before reactive hipsters were even born. Nevermind, java.io
also follows open-closed principle: one can simply enhance existing I/O code without touching built-in classes - but by plugging in new decorators. So I created a simple decorator for InputStream
that slows down reading resource on our side in order to enforce given download speed. I am using my favorite RateLimiter
class:public class ThrottlingInputStream extends InputStream {Arbitrary
private final InputStream target;
private final RateLimiter maxBytesPerSecond;
public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
this.target = target;
this.maxBytesPerSecond = maxBytesPerSecond;
}
@Override
public int read() throws IOException {
maxBytesPerSecond.acquire(1);
return target.read();
}
@Override
public int read(byte[] b) throws IOException {
maxBytesPerSecond.acquire(b.length);
return target.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
maxBytesPerSecond.acquire(len);
return target.read(b, off, len);
}
//less important below...
@Override
public long skip(long n) throws IOException {
return target.skip(n);
}
@Override
public int available() throws IOException {
return target.available();
}
@Override
public synchronized void mark(int readlimit) {
target.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
target.reset();
}
@Override
public boolean markSupported() {
return target.markSupported();
}
@Override
public void close() throws IOException {
target.close();
}
}
InputStream
can be wrapped with ThrottlingInputStream
so that reading is actually slowed down. You can either create new RateLimiter
per each ThrottlingInputStream
or one global, shared by all downloads. Of course one might argue that simple sleep()
(what RateLimiter
does underneath) wastes a lot of resources, but let's keep this example simple and avoid non-blocking I/O. Now we can easily plug decorator in:private InputStreamResource buildResource(FilePointer filePointer) {Example above limits download speed to 64 KiB/s - obviously in real life you would want to have such number configurable, preferably at runtime. BTW we already talked about the importance of
final InputStream inputStream = filePointer.open();
final RateLimiter throttler = RateLimiter.create(64 * FileUtils.ONE_KB);
final ThrottlingInputStream throttlingInputStream = new ThrottlingInputStream(inputStream, throttler);
return new InputStreamResource(throttlingInputStream);
}
Content-Length
header. If you monitor the progress of download with pv
, it will correctly estimate remaining time, which is a nice feature to have:~ $ curl localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b | pv > /dev/nullAs an extra bonus
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 71.2M 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 16kB 0:00:01 [14,8kB/s]
0 71.2M 0 40960 0 0 30097 0 0:41:21 0:00:01 0:41:20 30095 80kB 0:00:02 [ 64kB/s]
0 71.2M 0 104k 0 0 45110 0 0:27:35 0:00:02 0:27:33 45106 144kB 0:00:03 [ 64kB/s]
0 71.2M 0 168k 0 0 51192 0 0:24:18 0:00:03 0:24:15 51184 208kB 0:00:04 [ 64kB/s]
0 71.2M 0 232k 0 0 54475 0 0:22:51 0:00:04 0:22:47 54475 272kB 0:00:05 [63,9kB/s]
0 71.2M 0 296k 0 0 56541 0 0:22:00 0:00:05 0:21:55 67476 336kB 0:00:06 [ 64kB/s]
0 71.2M 0 360k 0 0 57956 0 0:21:28 0:00:06 0:21:22 65536 400kB 0:00:07 [ 64kB/s]
0 71.2M 0 424k 0 0 58986 0 0:21:06 0:00:07 0:20:59 65536 464kB 0:00:08 [ 64kB/s]
0 71.2M 0 488k 0 0 59765 0 0:20:49 0:00:08 0:20:41 65536 528kB 0:00:09 [ 64kB/s]
0 71.2M 0 552k 0 0 60382 0 0:20:36 0:00:09 0:20:27 65536 592kB 0:00:10 [ 64kB/s]
0 71.2M 0 616k 0 0 60883 0 0:20:26 0:00:10 0:20:16 65536 656kB 0:00:11 [ 64kB/s]
0 71.2M 0 680k 0 0 61289 0 0:20:18 0:00:11 0:20:07 65536 720kB 0:00:12 [ 64kB/s]
pv
proved our throttling works (last column). Now without Content-Length
pv
is clueless about the actual progress:~ $ curl localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b | pv > /dev/nullWe see that the data is flowing, but we have no idea how much has left. Thus
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 16384 0 16384 0 0 21116 0 --:--:-- --:--:-- --:--:-- 21113 32kB 0:00:01 [ 31kB/s]
100 81920 0 81920 0 0 46149 0 --:--:-- 0:00:01 --:--:-- 46126 96kB 0:00:02 [ 64kB/s]
100 144k 0 144k 0 0 53128 0 --:--:-- 0:00:02 --:--:-- 53118 160kB 0:00:03 [ 64kB/s]
100 208k 0 208k 0 0 56411 0 --:--:-- 0:00:03 --:--:-- 56406 224kB 0:00:04 [ 64kB/s]
100 272k 0 272k 0 0 58328 0 --:--:-- 0:00:04 --:--:-- 58318 288kB 0:00:05 [ 64kB/s]
100 336k 0 336k 0 0 59574 0 --:--:-- 0:00:05 --:--:-- 65536 352kB 0:00:06 [ 64kB/s]
100 400k 0 400k 0 0 60450 0 --:--:-- 0:00:06 --:--:-- 65536 416kB 0:00:07 [ 64kB/s]
100 464k 0 464k 0 0 61105 0 --:--:-- 0:00:07 --:--:-- 65536 480kB 0:00:08 [ 64kB/s]
100 528k 0 528k 0 0 61614 0 --:--:-- 0:00:08 --:--:-- 65536 544kB 0:00:09 [ 64kB/s]
100 592k 0 592k 0 0 62014 0 --:--:-- 0:00:09 --:--:-- 65536 608kB 0:00:10 [ 64kB/s]
100 656k 0 656k 0 0 62338 0 --:--:-- 0:00:10 --:--:-- 65536 672kB 0:00:11 [ 64kB/s]
100 720k 0 720k 0 0 62612 0 --:--:-- 0:00:11 --:--:-- 65536 736kB 0:00:12 [ 64kB/s]
Content-Length
is a really important header to have.Writing a download server
- Part I: Always stream, never keep fully in memory
- Part II: headers: Last-Modified, ETag and If-None-Match
- Part III: headers: Content-length and Range
- Part IV: Implement
HEAD
operation (efficiently) - Part V: Throttle download speed
- Part VI: Describe what you send (Content-type, et.al.)
Tags: HTTP, scalability, spring, spring mvc