Page MenuHomePhabricator

Experiment: set a short TTL on thumbnails for newly-uploaded images
Open, HighPublic

Assigned To
Authored By
Cparle
Aug 4 2026, 11:02 AM
Referenced Files
F100214632: image.png
Wed, Aug 26, 1:19 PM
F98436992: image.png
Fri, Aug 14, 10:16 AM
F98068455: image.png
Tue, Aug 11, 1:19 PM
F98053176: output_plot.png
Tue, Aug 11, 10:30 AM
F97776922: output_plot.png
Sun, Aug 9, 4:17 PM
F97499058: image.png
Fri, Aug 7, 4:10 PM
F97494499: image.png
Fri, Aug 7, 3:36 PM

Description

A great many media files have thumbnails generated on upload which are not accessed again (see graphs below)

So ...
When we're generating a thumbnail
If we set a TTL on the generated file in Swift, if and only if it has been uploaded recently
Then we might hope to see large reduction in thumbnail growth in return for a small increase in load on the thumbnail generation engine

The code

There's already some code around expiring objects in Swift in wikimedia_thumbor/result_storage/swift/swift.py, see SWIFT_THUMBNAIL_EXPIRY_SECONDS

Here's what Claude suggests

1. Capture the original media file's age

wikimedia_thumbor/loader/swift/__init__.py, in load() right after the successful get_object

# Swift returns the object's creation time as x-timestamp (epoch float,
# lowercased by swiftclient). Stash it for the result storage's TTL decision.
try:
    context.wikimedia_original_timestamp = float(headers.get('x-timestamp'))
except (TypeError, ValueError):
    context.wikimedia_original_timestamp = None
2. Store the result with a conditional expiry

wikimedia_thumbor/result_storage/swift/swift.py, in put() where the existing SWIFT_THUMBNAIL_EXPIRY_SECONDS block sits

expiry = self.context.config.get('SWIFT_THUMBNAIL_EXPIRY_SECONDS', 0)
jitter = self.context.config.get('SWIFT_THUMBNAIL_EXPIRY_JITTER_SECONDS', 0)
age_threshold = self.context.config.get(
    'SWIFT_THUMBNAIL_SPECULATIVE_AGE_SECONDS', 0)
containers = self.context.config.get(
    'SWIFT_THUMBNAIL_EXPIRY_CONTAINERS', [])

original_ts = getattr(self.context, 'wikimedia_original_timestamp', None)

if (
    expiry > 0
    and age_threshold > 0
    and original_ts is not None
    and self.context.wikimedia_thumbnail_container in containers
    and time.time() - original_ts < age_threshold
):
    headers['X-Delete-After'] = str(int(expiry + random.uniform(0, max(jitter, 0))))

Claude also says "Config lands wherever the other SWIFT_* values live in the thumbor deployment chart" ... I guess that's in puppet/modules/netbox/templates/configuration.py.erb but I don't know

SWIFT_THUMBNAIL_EXPIRY_CONTAINERS is a list of containers that are gonna be the experiment containers. Probably we should pick just one, and it needs to be one that hasn't had the delete script run recently (within the last month or pref two or more) so that it's not getting a big growth spike from that.

SWIFT_THUMBNAIL_SPECULATIVE_AGE_SECONDS is the threshold for the age of original - if an original is younger than this number of seconds then we set a TTL. Looking at the graphs below I'd say we try this at 86400 seconds (i.e. 1 day) first

SWIFT_THUMBNAIL_EXPIRY_SECONDS is the short TTL we'll set for new uploads. Looking at the graphs I think it's worth trying this at 172800 seconds (i.e. 48 hours) first

SWIFT_THUMBNAIL_EXPIRY_JITTER_SECONDS is so that if a bunch of thumbnails get created at the same time they don't all expire at the same time and cause a thumbor load spike. Honestly I'm not sure if we need this, might be simpler to leave it out for now (or leave it in, or set it to zero, I don't mind!)

We can turn the experiment off by setting SWIFT_THUMBNAIL_EXPIRY_CONTAINERS to empty or SWIFT_THUMBNAIL_SPECULATIVE_AGE_SECONDS to zero, but note that any TTLs set will still be in force, and the experiment won't fully end until anything with a TTL has expired.

Some unit tests would be really good here btw

Testing

Once the above is done and merged it's probably a good idea just to run a manual test with a new upload and check its X-Delete-After

Monitoring
  • thumbnail growth in the experimental container versus control container(s) (again pick one that hasn't had the delete script run on it recently) - expecting it to be significantly reduced (75%)
  • proportion of generated / stored-in-object-store thumbnails in the experimental container versus control container(s) - expecting generated to grow, but not by a lot (<10%, should not increase over time)
  • p50/p75/p95/p99 response times in the experimental container versus control container(s) - expecting p95 and p99 to grow, but only by a little
  • in grafana keep an eye on rate(thumbor_swift_thumbnail_exception_count{operation="write"}[5m]) to make sure the new code isn't causing issues with swift

Note that we won't see any changes in thumbnail growth or thumbor load or latency until AFTER the first newly-created thumbnails TTLs start to expire.


Some graphs for uploads on May 1 2026

thumbs requested then not requested again for _30 days.png (600×371 px, 14 KB)
Distinct thumbnail requests, any size (uploads = 76524).png (600×371 px, 20 KB)
Originals that had a thumbnail requested (uploads = 76524).png (600×371 px, 20 KB)

Event Timeline

For measuring growth, we can run all our measurements and clean ups on one container only and simply multiply it by 256. That way it won't be impacted by my clean up script (since it's doing it container by container)

I don't see a way to distinguish between containers in the metrics ... maybe I'm missing something though?

I don't see a way to distinguish between containers in the metrics ... maybe I'm missing something though?

We could probably make swift Prometheus exporter expose that or worst case, I can just write a screen to wake up, record timestamp and size of the given container and then sleep for an hour.

[ aside: we've previously talked about using TTL-based-thumb expiry in the past and decided against implementing it, some discussion was in T211661 . Which is obviously not the same as you're proposing here, but seemed worth mentioning]

[ aside: we've previously talked about using TTL-based-thumb expiry in the past and decided against implementing it, some discussion was in T211661 . Which is obviously not the same as you're proposing here, but seemed worth mentioning]

Yeah but also a lot has happened since then: We now have standard thumbnail sizes (and only those are allowed) which changes the equation a lot. Plus buying more hardware for a dedicated extra cache cluster is much more expensive than it used to be (that's my preferred way for storing thumbnails but until we can buy hardware at reasonable prices again, I don't think it's feasible)

I've started a script to collect stats every hour on ms-fe1009 on wikipedia-commons-local-thumb.11 and put it in a file. It can be useful to have a sense of how it is now before moving to setting a TTL.

BTW. This is the result in the past couple of hours:

timestamp,container,objects,bytes
2026-08-04T23:39:21Z,wikipedia-commons-local-thumb.11,826161,125645804778
2026-08-05T00:39:21Z,wikipedia-commons-local-thumb.11,827281,125839555913
2026-08-05T01:39:22Z,wikipedia-commons-local-thumb.11,828346,126050529701
2026-08-05T02:39:22Z,wikipedia-commons-local-thumb.11,829303,126253685629
2026-08-05T03:39:22Z,wikipedia-commons-local-thumb.11,830322,126416687634
2026-08-05T04:39:23Z,wikipedia-commons-local-thumb.11,831371,126580924160
2026-08-05T05:39:23Z,wikipedia-commons-local-thumb.11,832808,126773030756
2026-08-05T06:39:23Z,wikipedia-commons-local-thumb.11,834134,126980815372
2026-08-05T07:39:24Z,wikipedia-commons-local-thumb.11,835512,127211421126
2026-08-05T08:39:24Z,wikipedia-commons-local-thumb.11,836877,127426823070
2026-08-05T09:39:24Z,wikipedia-commons-local-thumb.11,838375,127665521613
2026-08-05T10:39:25Z,wikipedia-commons-local-thumb.11,839830,127946941352
2026-08-05T11:39:25Z,wikipedia-commons-local-thumb.11,841240,128208915216

Note that it grows rather fast since it's been fully purged recently. We need to wait for a bit to see how the growth develops.

Here's a little table of number of thumbs created in the container per day (once I'd dug out the old ones with '\n' and other such delights in the name):

mvernon@ms-be1072:~$ sudo sqlite3 --readonly /srv/swift-storage/accounts0/containers/56489/594/dca97818fea217d3227cd186d32df594/dca97818fea217d3227cd186d32df594.db "SELECT CAST(round(julianday('now')-julianday(created_at,'unixepoch')) AS INT) AS age, count(*) from object WHERE deleted=0 GROUP BY age ORDER BY age DESC;"
17|2152
16|46173
15|80742
14|94689
13|99122
12|88016
11|70032
10|53087
9|47634
8|51107
7|44935
6|42651
5|38112
4|35010
3|35796
2|31655
1|30687
0|14415

And here's a plot of that:

image.png (640×480 px, 22 KB)

So you can see after an initial peak, growth is currently around 30k thumbs per day but gradually declining. I can run this again later to update the plot if that's helpful :)

That's brilliant, thank you @MatthewVernon ! Any chance you could run the same on a bucket that got cleaned up a long time ago, e.g. 0f. Just so we can get a sense of the background growth rate

In the past couple of hours since I started gathering the metrics, this is the result:

timestamp,container,objects,bytes
2026-08-07T13:48:44Z,wikipedia-commons-local-thumb.0f,7366118,1226886590697
2026-08-07T14:48:44Z,wikipedia-commons-local-thumb.0f,7366541,1226993774270
2026-08-07T15:48:44Z,wikipedia-commons-local-thumb.0f,7366788,1227036258765

That means 8K per day per container (2M new thumbs in total)

of course it'd be better to wait a bit to get a more accurate metric (or look at the backends as Matthew did)

I don't think 0f has been effectively cleared out in eqiad at least - the oldest objects are from 2013, so the age plot is much less useful...

image.png (1,230×770 px, 52 KB)

I tried looking at another container - wikipedia-commons-local-thumb.3f in codfw. After clearing out objects with silly names that I'd expect the cleanup to have missed:

"thumbor/3/3f/Macroplaza,_Edificio_Latino,_Tribunal_de_Justicia_de_Nuevo_León.jpg/800px-\nMacroplaza,_Edificio_Latino,_Tribunal_de_Justicia_de_Nuevo_León.jpg"
"thumbor/3/3f/Puppies_Fighting.jpg/200px-\r\nPuppies_Fighting.jpg"
"3/3f/Macroplaza,_Edificio_Latino,_Tribunal_de_Justicia_de_Nuevo_León.jpg/800px-\nMacroplaza,_Edificio_Latino,_Tribunal_de_Justicia_de_Nuevo_León.jpg"
"3/3f/Puppies_Fighting.jpg/200px-\r\nPuppies_Fighting.jpg"

I'm still finding objects from 2021, long before the container should have been emptied:

mvernon@ms-be1078:~$ sudo sqlite3 --readonly /srv/swift-storage/accounts1/containers/43707/99b/aabb8f61febc75b421978955db0c999b/aabb8f61febc75b421978955db0c999b.db "SELECT rowid,created_at,date(created_at,'unixepoch'),name,deleted FROM object where created_at='1613409859.86478' AND deleted=0;"
8727665|1613409859.86478|2021-02-15|3/3f/Popular_Science_Monthly_Volume_2.djvu/page773-1024px-Popular_Science_Monthly_Volume_2.djvu.jpg|0

Just to confirm that's a genuine object...

root@ms-fe1009:~# swift stat wikipedia-commons-local-thumb.3f 3/3f/Popular_Science_Monthly_Volume_2.djvu/page773-1024px-Popular_Science_Monthly_Volume_2.djvu.jpg
               Account: AUTH_mw
             Container: wikipedia-commons-local-thumb.3f
                Object: 3/3f/Popular_Science_Monthly_Volume_2.djvu/page773-1024px-Popular_Science_Monthly_Volume_2.djvu.jpg
          Content Type: image/jpeg
        Content Length: 515462
         Last Modified: Mon, 15 Feb 2021 17:24:20 GMT
                  ETag: 49081a23279990cd7c73d413e93ea1a3
   Content-Disposition: inline;filename*=UTF-8''Popular_Science_Monthly_Volume_2.djvu.jpg
           X-Timestamp: 1613409859.86478
         Accept-Ranges: bytes
            X-Trans-Id: txd347d37ff8fa4dd8ace27-006a760a0a
X-Openstack-Request-Id: txd347d37ff8fa4dd8ace27-006a760a0a

Every container have been cleaned up at least partially. Otherwise, they would have end up with 10M objects (given the original per container number) but even 0f is 7M so 2/3rd of the "original" size. The reason we still have many old objects from old times is that many clean ups were interrupted by reboot of the frontend server. That's why I'm doing another round in T379942

This is 0f after two days

output_plot.png (1,500×900 px, 123 KB)

i.e. 260 per container per hour = 1.6M thumbs per day in total.

After a couple of days:

output_plot.png (1,500×900 px, 117 KB)

You can compare it with: T431721#12195066

@Ladsgroup do you have an older container where you're confident the initial deletion went to completion that I might usefully graph?

In the mean time, here's .11's graph updated - still not convinced it's linear yet...

image.png (640×480 px, 22 KB)

I can't say with certainty sorry.

Since it's Friday, here's an update to the .11 graph:

image.png (640×480 px, 21 KB)

Thanks @MatthewVernon

@Ladsgroup I don't understand what the x-axis is on your graphs

Thanks @MatthewVernon

@Ladsgroup I don't understand what the x-axis is on your graphs

That's hours since the start of being recorded. i.e. my last graph shows growth for four days.