Compress plain GPX uploads with gzip - #7124
Conversation
|
If the majority of downloads are of the uncompressed file, am I right to think that this will translate in costs to the infrastructure? Compute of the server-side decompression and traffic. If so, can this be significant enough to be a problem? Should the "download gpx" link be slightly more "hidden" to avoid this situation? Just an additional click or something like that. |
|
@pablobm you're right that this could add real bandwidth and CPU cost. I looked into it a bit more. I think we can avoid it without hiding the link, using the Browsers always send Accept-Encoding: gzip, so this happens on its own and the user still gets a normal .gpx I tested it with curl --compressed, which behaves like a browser: If a client doesn't support gzip, the server unzips the file and sends it plain, same as today, so nothing breaks. Other formats work the same way too: we still serve whatever the user uploaded. And since the backend decides this from the request header, one download link is enough now, so I kept just one. |
pablobm
left a comment
There was a problem hiding this comment.
Serving it directly sounds like a good idea! From a quick online lookup, it seems that even MS Edge should support it 👍
|
I changed the logic a bit. There is now a The user still gets a plain GPX either way. If their client accepts gzip, the server sends the gzip and the client unzips it. If not, the server unzips it first. And the CPU usage already happens today when clients request data.gpx, because the server unzips every file requested with .gpx. |
tomhughes
left a comment
There was a problem hiding this comment.
Please review the commit history and squash any fixup commits into the things they are fixing so that I can attempt to review this - as it is it's impossible because I find things to comment on and then when I go to leave a comment I find it's no longer there because it's been changed in a later commit.
pablobm
left a comment
There was a problem hiding this comment.
The PR description is now out of date, with the mention of Windows users, etc. Mind updating it? It'll make it easier to future readers.
General question: do we have any idea of how common is it to receive requests for the xml/gpx versions of the traces? The request.format == Mime[:xml], etc.
The website doesn't link to those, so I guess any website visitors will get the trace.file.attached? version. However the API docs do describe this option:
The response will always be a GPX format file if you use a .gpx URL suffix, a XML file in an undocumented format if you use a .xml URL suffix, otherwise the response will be the exact file that was uploaded.
Is this something that is very common and could generate this extra effort on our side, decompressing every time?
|
Thinking a bit more about this. Would the I seem to remember there was a way to have S3 store gzipped files and serve them as |
Good point, I looked at the Content-Encoding on S3 idea, but ActiveStorage's upload method can not set that header on the file. It can set content_type and some custom metadata and usea a prefix: |
615368e to
7ae3cb0
Compare
|
I squashed into 2 commits, ready for another look, thanks. |
My concern then is that this introduces new steps in this effort. If we do as you say, then we'll still need the So the steps would be:
This sounds a bit more complex. What do Ops think? Would it be worth doing properly the first time? |
|
yeah, a custom uploader that stores the file on S3 with The download side gets simpler too. If the client accepts gzip, we redirect to S3 and it will server the file, so the app stays out of the way. If a client doesn't support gzip, the server unzips it and sends it plain. The server already does that today when someone asks for /data.gpx, so it's not new behavior, For the old files, once the new compression is running, we could add a background job that takes each plain GPX, gzips it, and re-uploads it with |
2e06976 to
272549a
Compare
|
I implemented the custom uploader. GpxS3Service sets Content-Encoding: gzip on upload, so S3 serves the file gzipped and the client unzips it. On download, the clients that accept gzip get a redirect to S3. I kept the server_gzipped flag for now, the server needs it to know when to unzip for clients without gzip. We can drop it once all the old plain GPX is migrated to gzip, but it is for other PR. |
pablobm
left a comment
There was a problem hiding this comment.
Something to mention in the description and the documentation: in storage.yml, now we'll need service: GpxS3.
|
Why do we need to monkey patch rails? We seem to support gzip compressed traces currently without any of that nonsense so I don't see why we need it for ones which we compress? I assume it's because of Windows not handling gzipped traces well and wanting to try and automatically decompress them again? But we've never worried about gzipped traces not being handled well on Windows before so why start now? |
|
Can somebody explain how |
It's not a monkey patch. The PR introduces a new storage adapter (
Set It's true that the code changes make the objective of this PR a bit unclear. The objective is making it so that we compress GPX files on upload (unless already compressed), in such a way that doesn't require us to decompress them back when serving them, because we delegate decompression to browsers via For comparison, I have a GPX file here (taken from the website at random) that compresses from 341kB to 24kB. Having said that, I don't know what the storage impact would be overall as I don't know how frequently users upload plain GPX files as opposed to compressed ones. |
Do you know if there's any documentation on writing new adapters like this? Does it need to be in the rails owned
So this doesn't actually use this by default, we will need to enable it in our production environment? What happens if somebody uses S3 and doesn't enable it? The traces still get compressed but don't get automatically decompressed on download?
I understand the goal is to compress otherwise uncompressed traces. What I'm questioning is whether we need to automatically decompress them give we already have compressed traces and don't do that for them. |
Right. As I see it, currently this is what happens:
This PR changes the first case:
End result: on download, users get exactly what they uploaded, avoiding confusion. We do something clever in the middle to compress/decompress with minimum impact to our infrastructure. |
To actually use it, we need switch the traces storage in chef from service: "S3" to service: "GpxS3" rails.rb#L67. It's the same standard the other services already use: avatars, traces and images are all set by name as service: "S3" today. I'll add the example config and a docs note, which are missing.
From what I saw in the default configs, Active Storage turns the config name into the class name. For example, S3 maps to S3Service, and if we set GpxS3 it maps to GpxS3Service. That's the default, so the name and the namespace have to match.
This PR code gzips every plain GPX and stores it gzipped. The only difference between S3 and GpxS3 is the header: S3 stores the file without Content-Encoding, and GpxS3 adds it. So when a client asks for the file:
Either way the user gets their plain GPX back. Nothing breaks without GpxS3, we just decompress on the server instead of letting the browser do it.
The goal in #4188 is to compress the plain GPX files that people upload, to save storage and transfer-out. The part I wanted to keep is that users get back what they uploaded. If a user uploads a .gpx, it downloads as .gpx today. If we store it as gz and serve it raw, that user gets a .gz instead, which changes the behavior. I don't think this differs from leaving user .gz uploads alone. The rule is the same: give back what was uploaded. If we're ok with plain GPX downloading as .gz, this gets much simpler: compress on upload, no custom adapter, no Content-Encoding, no flag. It's more of a product call, and I'm happy to go that way if the decision is to download as .gz. |
272549a to
2f4fbf4
Compare
I've looked at this in more detail and while it may not technically be a monkey patch in the traditional sense it's functionally equivalent to one in terms of how it works and the maintenance risk it creates. As far as I can tell adding your own services is not an intended customisation point in the rails code base, and if it was they would probably have made it possible without having to inject code into their namespace. That isn't the main problem though - the main problem is that this code is essentially copy and pasted from the code in the existing S3 service, except that for some reason it's been changed from using named parameters to using a generic options argument for most things, and the multipart upload support has been removed, though by default that is only used for files over 100Mb in size so that might be reasonable. The result though is that any change to the internals of the current S3 service will need to be reflected here which creates a maintenance headache, especially for things that don't cause obvious immediate breakage. That said I don't see any better way to set the Content-Encoding argument on upload 😢 |
tomhughes
left a comment
There was a problem hiding this comment.
The third commit should probably be squashed into the first one?
|
@tomhughes, You're right, copying the whole upload was too much. It also had a real problem. It forced a single-part PUT for every upload, so large .zip or .tar traces skipped multipart. I reworked it, the GpxS3Service no longer overrides the public upload. It now overrides only I also added the missing example in example.storage.yml and a note in settings.yml. |
376b8c4 to
3424667
Compare
pablobm
left a comment
There was a problem hiding this comment.
I think it's worth creating a PR for Rails to support content_encoding here. I had a quick look and there doesn't seem to be any issue or PR.
That might not go anywhere, or perhaps it does. In any case it could give us some additional insight, if there's some detail we are overlooking. It doesn't need to block this PR: if in the future it's merged, we can drop the custom service.
The check is needed. Compression happens in the model on upload, so gzipped_by_server? is true for any storage. The Content-Encoding header only comes from GpxS3. So with plain S3 or Disk, the file is gzipped (gzipped_by_server? true) but there is no header (store_serves_gzip? false). That is also the current default, since GpxS3 is not enabled yet. Without the store_serves_gzip? check we would redirect and the client gets the raw gzip named .gpx with no Content-Encoding, so a broken file. The check sends those cases to send_data so the server unzips first. It is the same case tomhughes asked about: using S3 without GpxS3. |
|
Here is an example, tied to what Tom asked about:
Let's say we merge this PR and run it on plain S3 for now, without GpxS3.A user uploads route.gpx:
Later the user downloads it:
Now let's say we enable GpxS3 later in the deployA user uploads route.gpx:
Later the user downloads it:
So the code redirects to S3. S3 serves the gzipped file with the header, and the browser unzips it. The server does no work. The user still gets route.gpx back. Either way the user gets a plain GPX. The only difference is who unzips: on plain S3 and Disk the server does it (send_data), on GpxS3 the browser does it (redirect). store_serves_gzip? is the check that picks between the two, so it is needed. All three storages compress the file. Only GpxS3 serves the header, so only there the browser unzips:
|
3424667 to
679b52b
Compare
|
I ran this on a deploy with GpxS3. I uploaded a plain GPX of about 200 KB. On S3 it is stored gzipped, only 22.7 KB, and with the Content-Encoding: gzip header.
https://osm-s3-website-dev.s3.us-east-1.amazonaws.com/krivwyyrck6usfbhjgmugyh461p2 On download, a browser gets the small file from S3 and unzips it on its own, so it still receives a normal GPX. A client that does not accept gzip gets the plain file, the server unzips it first. Either way the file is the same one I uploaded. BASE=https://compress-gpx-uploads.167.233.138.142.nip.io
# like a browser: it fetches the small gzipped file and unzips it
> curl -s --compressed -L "$BASE/traces/2/data" -o out.gpx -w "on the wire: %{size_download} bytes\n"
# on the wire: 23202 bytes
# see the header S3 sends
> curl -s -H "Accept-Encoding: gzip" -D - -L "$BASE/traces/2/data" -o /dev/null | grep -i content-encoding
# content-encoding: gzip
# a client without gzip: the server unzips it and sends the plain 200 KB
curl -s -H "Accept-Encoding: identity" -L "$BASE/traces/2/data" -o plain.gpx -w "on the wire: %{size_download} bytes\n"
# on the wire: 200176 bytes |
|
I think something is wrong. Scenario:
|
|
You're right, the check depended on the storage config, but the header is a property of each file, set once at upload time. to that approach won't work properly I changed it to a per-file flag on upload, the model sets I've also added tests for these cases 👇 :
The scenario, step by step, what you highlight above:
Files uploaded before enabling GpxS3 keep the server unzip path, later we can add a small backfill task to move them to the redirect path. The same backfill can also handle the old traces from before this PR, the ones without server_gzipped or gzip_content_encoding. I think that can be a other PR. |
tomhughes
left a comment
There was a problem hiding this comment.
Please squash the relevant parts of the third commit into the first two... I'm not sure exactly what it's doing because I gave up reviewing when I realised that commit was completely changing how things work which is my point, that I don't want to waste my time reviewing changes only to find a later commit is going to remove them!
I'd certainly be much happier with this if it looked like we were on a path to not needing the custom service. |
0dc3703 to
3163f26
Compare
I squashed into two commits, the first adds the GpxS3 service, the second stores plain GPX gzipped and serves it with a redirect, it is ready for another look! |
3163f26 to
68c848d
Compare
|
Thanks @pablobm, I've applied both changes, the settings.yml comment and the multipart test coverage and I squashed them into the GpxS3 service commit. |
I opened a topic on the Rails core forum to propose per-file |


This PR gzips plain GPX files before they go to S3, which reduce a big part of their size. Files that are already compressed (gzip, bzip2, zip, tar) are the same.
The user still gets a plain .gpx, so the download link doesn't change for them. When the app stores a plain GPX gzipped, it sets server_gzipped = true in the blob metadata. On download it works in two ways:
If the client accepts gzip, the server sends the gzipped file as is with
Content-Encoding: gzipand the client unzips it, the server skips the decompression, so it saves CPU and most browsers support Content-Encoding: gzip today.If the client does not support gzip, the server unzips it first and sends plain GPX. This is the same work the server already does today when a client requests
/data.gpxFor the server-side unzip I reused the existing xml_file function, so there's no new decompression code. The web and API downloads share it through a small concern.
Ref: #4188
cc. @1ec5