32

tl;dr A Drive API call returns a failure status (403) even though the request was successfully processed.

I insert 100 files in a loop. For this test I have DISABLED backoff and retry, so if an insert fails with a 403, I ignore it and proceed with the next file. Out of 100 files, I get 63 403 rate limit exceptions.

However, on checking Drive, of those 63 failures, 3 actually succeeded, ie. the file was created on drive. Had I done the usual backoff and retry, I would have ended up with duplicated inserts. This confirms the behaviour I was seeing with backoff-retry enabled, ie. from my 100 file test, I am consistently seeing 3-4 duplicate insertions.

It smells like there is an asynchronous connection between the API endpoint server and the Drive storage servers which is causing non-deterministic results, especially on high volume writes.

Since this means I can't rely on "403 rate limit" to throttle my inserts, I need to know what is a safe insert rate so as not to trigger these timing bugs.

Running the code below, gives ...

Summary...
File insert attempts (a)       = 100
rate limit errors (b)          = 31
expected number of files (a-b) = 69
Actual number of files         = 73 

code...

package com.cnw.test.servlets;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.ChildList;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.File.Labels;
import com.google.api.services.drive.model.ParentReference;

import couk.cleverthinking.cnw.oauth.CredentialMediatorB;
import couk.cleverthinking.cnw.oauth.CredentialMediatorB.InvalidClientSecretsException;

@SuppressWarnings("serial")
    /**
     * 
     * AppEngine servlet to demonstrate that Drive IS performing an insert despite throwing a 403 rate limit exception.
     * 
     * All it does is create a folder, then loop to create x files. Any 403 rate limit exceptions are counted.
     * At the end, compare the expected number of file (attempted - 403) vs. the actual.
     * In a run of 100 files, I consistently see between 1 and 3 more files than expected, ie. despite throwing a 403 rate limit,
     * Drive *sometimes* creates the file anyway.
     * 
     * To run this, you will need to ...
     * 1) enter an APPNAME above
     * 2) enter a google user id above
     * 3) Have a valid stored credential for that user
     * 
     * (2) and (3) can be replaced by a manually constructed Credential 
     * 
     * Your test must generate rate limit errors, so if you have a very slow connection, you might need to run 2 or 3 in parallel. 
     * I run the test on a medium speed connection and I see 403 rate limits after 30 or so inserts.
     * Creating 100 files consistently exposes the problem.
     * 
     */
public class Hack extends HttpServlet {

    private final String APPNAME = "MyApp";  // ENTER YOUR APP NAME
    private final String GOOGLE_USER_ID_TO_FETCH_CREDENTIAL = "11222222222222222222222"; //ENTER YOUR GOOGLE USER ID
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        /*
         *  set up the counters
         */
        // I run this as a servlet, so I get the number of files from the request URL
        int numFiles = Integer.parseInt(request.getParameter("numfiles"));
        int fileCount = 0;
        int ratelimitCount = 0;

        /*
         * Load the Credential
         */
        CredentialMediatorB cmb = null;
        try {
            cmb = new CredentialMediatorB(request);
        } catch (InvalidClientSecretsException e) {
            e.printStackTrace();
        }
        // this fetches a stored credential, you might choose to construct one manually
        Credential credential = cmb.getStoredCredential(GOOGLE_USER_ID_TO_FETCH_CREDENTIAL);

        /*
         * Use the credential to create a drive service
         */
        Drive driveService = new Drive.Builder(new NetHttpTransport(), new JacksonFactory(), credential).setApplicationName(APPNAME).build();

        /* 
         * make a parent folder to make it easier to count the files and delete them after the test
         */
        File folderParent = new File();
        folderParent.setTitle("403parentfolder-" + numFiles);
        folderParent.setMimeType("application/vnd.google-apps.folder");
        folderParent.setParents(Arrays.asList(new ParentReference().setId("root")));
        folderParent.setLabels(new Labels().setHidden(false));
        driveService.files().list().execute();
        folderParent = driveService.files().insert(folderParent).execute();
        System.out.println("folder made with id = " + folderParent.getId());

        /*
         * store the parent folder id in a parent array for use by each child file
         */
        List<ParentReference> parents = new ArrayList<ParentReference>();
        parents.add(new ParentReference().setId(folderParent.getId()));

        /*
         * loop for each file
         */
        for (fileCount = 0; fileCount < numFiles; fileCount++) {
            /*
             * make a File object for the insert
             */
            File file = new File();
            file.setTitle("testfile-" + (fileCount+1));
            file.setParents(parents);
            file.setDescription("description");
            file.setMimeType("text/html");

            try {
                System.out.println("making file "+fileCount + " of "+numFiles);
                // call the drive service insert execute method 
                driveService.files().insert(file).setConvert(false).execute();
            } catch (GoogleJsonResponseException e) {
                GoogleJsonError error = e.getDetails();
                // look for rate errors and count them. Normally one would expo-backoff here, but this is to demonstrate that despite
                // the 403, the file DID get created
                if (error.getCode() == 403 && error.getMessage().toLowerCase().contains("rate limit")) {
                    System.out.println("rate limit exception on file " + fileCount + " of "+numFiles);
                    // increment a count of rate limit errors
                    ratelimitCount++;
                } else {
                    // just in case there is a different exception thrown
                    System.out.println("[DbSA465] Error message: " + error.getCode() + " " + error.getMessage());
                }
            }
        }

        /* 
         * all done. get the children of the folder to see how many files were actually created
         */
        ChildList children = driveService.children().list(folderParent.getId()).execute();

        /*
         * and the winner is ...
         */
        System.out.println("\nSummary...");
        System.out.println("File insert attempts (a)       = " + numFiles);
        System.out.println("rate limit errors (b)          = " + ratelimitCount);
        System.out.println("expected number of files (a-b) = " + (numFiles - ratelimitCount));
        System.out.println("Actual number of files         = " + children.getItems().size() + " NB. There is a limit of 100 children in a single page, so if you're expecting more than 100, need to follow nextPageToken");
    }
}
pinoyyid
  • 21,499
  • 14
  • 64
  • 115
  • 1
    "It smells like there is an asynchronous connection between the API endpoint server and the Drive storage servers which is causing non-deterministic results." This is not true, API is a thin layer over our storage backend and everything is synchronous. API rejects the requests on a very high level that it shouldn't be even hitting the storage service layer. I'm investigating the issue. There could be another racing condition or a problem with the lock server though. – Burcu Dogan Sep 04 '13 at 09:06
  • Many thanks Burcu. Very much appreciated – pinoyyid Sep 04 '13 at 09:12
  • On the other hand children listing returns trashed items as well, are you sure that all files under that folder are created in this batch? – Burcu Dogan Sep 04 '13 at 09:16
  • If you check the code, you'll see that the folder is created for each test. Therefore "children", are specific to each test run. – pinoyyid Sep 04 '13 at 11:20
  • 2
    Ran similar test code myself a few weeks back and observed the same thing -- out of 100 or so files, averaged 2-3 duplicate inserts. – Steve Bazyl Sep 11 '13 at 21:56
  • 1
    Is there any progress on this bug? – pinoyyid Oct 16 '13 at 18:12
  • 2
    I also see this bug right now and wonder if there have been any progress to this? – Andreas Mattisson Apr 22 '14 at 19:29
  • 1
    Still getting the same result – Vlad Tsepelev Sep 09 '14 at 15:29
  • 1
    Same here -- using a Service account and inserting thousands of files in a loop, handling errors and doing backoff-retry, but still there are duplicates and an inordinate amount of 403 - errors even at startup – Don Cheadle Feb 03 '15 at 20:42
  • I've seen this before when creating a folder and it may still be happening - we had to implement a complex 'assume GDrive still create folder after we get an error' workaround as it was affecting users quite a lot – RichVel Feb 09 '15 at 10:59
  • 2
    https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?can=2&start=0&num=100&q=rate%20limit&colspec=Stars%20Opened%20ID%20Type%20Status%20Summary%20API%20Owner&groupby=&sort=&id=3774 – pinoyyid Mar 17 '15 at 15:38
  • I'm getting this to epic levels in my app (which processes about 40,000 files over 24 hours). It's just sad this is such a problem. 403 usage error returned implies that the request was NOT fulfilled. Is that link @pinoyyid the only case / reference to this issue? Has Google cmmented on this at all – Don Cheadle Mar 19 '15 at 23:36
  • @BurcuDogan any news? A note: I get this issue when using Java SDK, not using "batch inserts" (but am inserting files very quickly, and doing backoff from 403 requests) – Don Cheadle Mar 20 '15 at 00:08
  • After seeing this issue I eventually just started checking for existence on failure. It's very cheap in comparison. – Ryan Oct 30 '15 at 00:18
  • Since backoff is part of the Google client library i'm curios as to how you disabled it? I was not aware that there was a setting for this or have you done something ZeroBackOffRequestInitializer like in the unit tests on the library? – Linda Lawton - DaImTo Aug 16 '21 at 09:17
  • Where is extension of thread class and the Id indexer for the thread job controller to prevent duplicative non synchronized out of sequence writes? – Samuel Marchant Sep 25 '22 at 13:33
  • @SamuelMarchant I think you might have misunderstood the code sample. There is only one thread. – pinoyyid Sep 25 '22 at 15:00
  • Only one thread running the classes? Or only one class extending thread? I did mean sequentially controlling each write by encapsulating it to unique and dissallow duplicate jobs occuring. And how many cores are assigned to the JVM? – Samuel Marchant Sep 25 '22 at 15:08
  • If you don't want duplicates , put the job in a thread with a unique index Id and synchronize all its methods and put synchronized blocks around external non synchronized methods. – Samuel Marchant Sep 25 '22 at 16:02
  • @SamuelMarchant you've misunderstood the problem. This isn't a java problem (I've now removed the java tag to avoid confusion). The duplication doesn't arise in the application code. It arises within the Google Drive infrastructure. – pinoyyid Sep 25 '22 at 16:18
  • Just catching on, are youc ontrolling a network card baude rate throttle? Probably more than one CPU core? You cannot simply use for or while loop, use atom int in concurrency, or use a controller thread to sub thread worker both set to max priority. – Samuel Marchant Sep 25 '22 at 16:19
  • The other problem may be more than one set of link paths in the network hops with intermediary servers holding the transfer job because of loading and cannot call back fast enough to another hop to say the job is completed. I don't see what throttle does if network congestion and job duplicates to find a hop link path to destination are appearing the problem. Does "tracert" get different results? – Samuel Marchant Sep 27 '22 at 06:08
  • You could also read Google's terms of service and contractual agreement policies, there may be a maximum speed allowed whether manually controlled or controlled by the server, quantity of files, quantity of data p/time. – Samuel Marchant Sep 27 '22 at 06:33
  • 3
    @SamuelMarchant again, you're kinda missing the point. The issue is not that insert requests are being rate limited. The issue is that some insert requests fail with a rate limit error, but the insert actually succeeded. The humble developer sees the failure, and retries, only to find that there are now two files. – pinoyyid Sep 27 '22 at 13:04

2 Answers2

1

I'm assuming you're trying to do Parallel downloads...

This may not be an answer you're looking for, but this is what I've experienced in my interactions with google drive api. I use C#, so it's a bit different, but maybe it'll help.

I had to set a specific amount of threads to run at one time. If I let my program run all 100 entries at one time as separate threads, I run into the rate limit error as well.

I don't know well at all, but in my C# program, I run 3 threads (definable by the user, 3 is default)

opts = new ParallelOptions { MaxDegreeOfParallelism = 3 };
var checkforfinished = 
Parallel.ForEach(lstBackupUsers.Items.Cast<ListViewItem>(), opts, name => {
{ // my logic code here }

I did a quick search and found that Java 8 (not sure if that's what you're using) supports Parallel().forEach(), maybe that'd help you. The resource I found for this is at: http://radar.oreilly.com/2015/02/java-8-streams-api-and-parallelism.html

Hope this helps, taking my turns trying to help others on SO as people have helped me!

Michael Tucker
  • 309
  • 3
  • 14
  • 2
    thx for the answer. The problem is all Google's, so there is very little that can be done at the client. I ended up creating a fairly complex backoff algorithm that slowed down after a 403, but then gradually speeded up until it happened again. After a few iterations it settles at a rate that G seems happy with. Running parallel updates is probably a bad idea since it's very difficult to rate limit over parallel threads. The only exception would be if each thread was using a different app id such that G didn't aggregate the arrival rates before deciding to throttle. – pinoyyid Nov 23 '16 at 00:38
1

There is no answer to this problem since it's a confirmed Drive bug.

For anybody experiencing the problem (which is anybody doing bulk inserts), the workaround is the following pseudo code...

as part of the File json for the insert,
 include a synthetic ID as a custom property. 
Eg file.setProperties("myID", filename+count++) // NB store the file object in a map/array

If an insert receives a 403, check if the insert actually succeeded
 with a query on the synthetic ID. 
Eg service.files().list().setQ("appProperties has { key='myID' and value='filenamecount' }") // where filenamecount is from the stored file object

If file.list returns a hit, the insert succeeded and no further action is required. 
If there are zero results, 
the 403 was accurate and the insert needs to be requeued. 

Note that the ONLY safe way to do bulk inserts is via a queue
 which you throttle in response to receiving 403 errors. 
Do not implement a simplistic exponential backoff.

pinoyyid
  • 21,499
  • 14
  • 64
  • 115