Okay, I figured it out.
It’s that old datastore bug where it throws timeout exceptions when you try to iterate on a .all() with a lot of items.
I’m using the workaround that I only iterate over some of them, then I set a cursor, do the .all() again, move forward to the cursor, and then keep going.
Usually that works, but sometimes it will still throw a timeout exception, so I have it in a task so it gets retried.
Something must have changed in the datastore, so the workaround was consistently NOT working around the problem. So the task ran over and over, eventually hitting some undocumented quota limit.
I just lowered the number of items I’ll go through, and that seems to have gotten the workaround working again.
-Joshua