实时API客户端重新连接失败

时间:2016-02-26 22:46:37

标签: javascript google-drive-realtime-api

在断开/连接WiFi后,我们已经看到我们的实时文档在一段时间内没有正确重新连接。特别是Safari在为我们重现这个问题时有100%的成功率。 Chrome也容易出现同样的问题,尽管它在重新连接Realtime API方面比Safari做得好得多。

该问题似乎与浏览器未连接到互联网时如何/如果绑定/保存XHR请求报告为JS失败有关。在Safari的情况下,这些请求永远不会完全失败,而是因为没有与XHR一起指定超时而坐下来等待。一旦发生这种情况,Realtime API就不会尝试重新连接。

以下是在Safari中100%重现此问题的文件。他们将提示等待/禁用/启用WiFi。每个步骤可能需要约10-20秒。其他信息在评论中。它们也可以直接从https://drive.google.com/file/d/0B1es-bMybSeSUmRqX2JJd3dqRFE/view?usp=sharing获得。可能需要修改ClientID和ApiKey以使其正常工作。

是否有其他人遇到类似问题或看到过可以解释的行为?除了检测这个案子并破坏文件之外,还有任何合理的解决方法吗?

编辑:当然在发布这个帖子后15分钟,一个潜在的解决方法突然出现在我脑海中。在任何想象中并不漂亮并且可能导致其他问题,但是劫持XHR以手动设置超时以确保请求到期似乎可以解决问题。 [Edit3:更新为70s超时的范围解决方法。]

解决方法

var timeoutURLs =
[
    'https://drive.google.com/otservice/save',
    'https://drive.google.com/otservice/bind'
];

function shouldSetXHRTimeout(xhr)
{
    var shouldSetTimeout = false;

    var URLMatch = xhr.url.split('?')[0];

    if (timeoutURLs.indexOf(URLMatch) >= 0)
    {
        shouldSetTimeout = true;
    }

    return shouldSetTimeout;
}

function wrapXHR()
{
    var __send = window.XMLHttpRequest.prototype.send;

    window.XMLHttpRequest.prototype.send = function (data)
    {
        if (!this.timeout && shouldWrapXHRTimeout(this)
        {
            this.timeout = 70000;
        }

        __send.call(this, data);
    };
};

wrapXHR();

gdrive_connect.html

<!DOCTYPE html>
<html>
<head>
    <title>Duchess</title>
    <script type="text/javascript" src="//apis.google.com/js/client.js?onload=GoogleApiLoaded" async defer></script>
    <script src="gdrive_connect.js" type="application/javascript" ></script>
</head>

<body>
    <div id="status">Setting up scenario...</div>
    <button id="button" onclick="runScenario()">Run Scenario</button>
</body>

gdrive_connect.js

var clientId = "597847337936.apps.googleusercontent.com";
var REALTIME_MIMETYPE = 'application/vnd.google-apps.drive-sdk';

//
// Problem
//  The realtime lib is not reconnecting itself and pushing changes back to the remote server
//  after getting disconnected from WiFi in some cases. That is, disconnecting WiFi then reconnecting can
//  result in the Realtime API no longer sending or receiving updates for the document that it believes is
//  connected.
//
// More Info
//  We spent a while tracking down the cause of the issue, and it appears to be with how the bind/save calls
//  for the Realtime API are handled if they just 'never return'. In the reconnection failure cases, it
//  appears that these network requests never completely 'fail' and instead indefinitely wait without
//  notifying the XHR handler.
//
// Hardware
//  This was reprod on a MacBook Pro w/ OSX 10.11 on both public WiFi as well as a home WiFi setup.
//
// Software
//  This issue repros in both Chrome and Safari. In Safari it happens 100% of the time and will be a
//  significantly more consistent repro. Chrome it only happens occasionally and can be fixed by opening and
//  closing the laptop. In Chrome this appears to have a different effect on how hung network requests are
//  handled than just disabling/enabling WiFi.
//
//   Safari
//     Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.56 (KHTML, like Gecko) Version/9.0 Safari/601.1.56
//     Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/601.4.4 (KHTML, like Gecko) Version/9.0.3 Safari/601.4.4
//   Chrome
//     Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36
//
// Repro
//  0) Ensure that the browser/machine are properly connected to the internet and authorized.
//  1) Authorize and load realtime doc.
//  2) Make modification to the document.
//  3) Verify modification was saved remotely.
//  4) Disable WiFi on machine.
//  5) Make modification to the document.
//  6) Verify that the modification did not get saved remotely as machine is offline.
//  7) Enable WiFi on machine.
//  8) Check document's saveDelay to verify that the remote change was not propagated.
//  9) Make additional modification to document.
//  10) Verify that none of the changes have been saved remotely.
//
// The repro requires that you manually disable/enable WiFi and should prompt when to do so. All other info
// is spit out into the console about all of the online/offline related status. When a non-network !!!ERRROR
// is displayed, this means that the repro has been successful. The browser is 100% online, the user is
// authenticated but the realtime document is not flushing its changes to the remote server.
//
// Additional Repro
//  While the repro page is enough to 100% consistently cause reconnection issues in Safari, Chrome will fix
//  itself in most cases. Disabling/Enabling the WiFi rapidly right after a 'bind' request is made will
//  generally also get Chrome stuck not reconnecting to the server.
//
// Console Key
//  Delay: The realtime doc's 'saveDelay' field.
//  Modified: Time since last modification was made to the doc.
//  IsOnline: Whether the browser thinks it is online (navigator.onLine).
//  DriveConn: Result of making a separate authenticated request to drive through gapi.
//  TokenValid: Whether the gapi.auth authroization token is still currently valid.
//  IsOpen: The realtime doc's '!isClosed' field.
//
// The realtime document is accessible through 'window.remoteDoc'.
//

var ReproSteps =
[
    'Setting up scenario...',       // 0
    'Click button to start',        // 1
    'Connecting...',                // 2
    'Running... please wait ~10s',  // 3
    'Disable WiFi',                 // 4
    'Enable WiFi',                  // 5
    'Running... please wait ~10s',  // 6
    'Done! See error in console'    // 7
];


var currentStep = 0;
function setReproStep(index)
{
    if (index >= currentStep)
    {
        var msg = ReproSteps[index];

        document.getElementById('status').innerText = msg;
    }
}

function GoogleApiLoaded()
{
    console.log('Google Remote Client Lib Loaded');

    gapi.auth.init();

    gapi.client.setApiKey('AIzaSyB9HEdSJ-nhLJG_ssSSqhI2DX74GSiKSao');

    gapi.load('auth:client,drive-realtime', function ()
    {
        console.log('GAPI Loaded Libs');

        setReproStep(1);
    });
}

function createRealtimeFile(title, description, callback)
{
    console.log('Creating Drive Document');
    gapi.client.drive.files.insert({
        'resource':
        {
            'mimeType': REALTIME_MIMETYPE,
            'title': title,
            'description': description
        }
    }).execute(function (docInfo)
    {
        callback(docInfo, /*newDoc*/true);
    });
}

function openRealtimeFile(title, callback)
{
    gapi.client.load('drive', 'v2', function ()
    {
        gapi.client.drive.files.list(
        {
            'q': 'title='+"'"+title+"' and 'me' in owners and trashed=false"
        }).execute(function (results)
        {
            if (!results.items || results.items.length === 0)
            {
                createRealtimeFile(title, /*DocDescription*/"", callback);
            }
            else
            {
                callback(results.items[0], /*newDoc*/false);
            }
        });
    });
}

function runScenario()
{
    console.log('Page Loaded');

    document.getElementById('button').style.display = 'none';

    setReproStep(2);

    var GScope =
    {
        Drive: 'https://www.googleapis.com/auth/drive.file'
    };

    var handleAuthResult = function (authResult)
    {
        console.log('Requesting Drive Document');

        openRealtimeFile("TESTDOC__", function (docInfo, newDoc)
        {
            if (docInfo && docInfo.id)
            {
                gapi.drive.realtime.load(docInfo.id, onDocLoaded, onDocInitialized, onDocLoadError);
            }
            else
            {
                console.log('Unable to find realtime doc');
                debugger;
            }
        });
    };

    gapi.auth.authorize(
    {
        client_id: clientId,
        scope: [ GScope.Drive ],
        immediate: false
    }, handleAuthResult);
}

function onDocInitialized(model)
{
    console.log('Drive Document Initialized');

    var docRoot = model.createMap();

    model.getRoot().set('docRoot', docRoot);
}

var testMap;
var docDataCounter = 0;
var lastWrite = 0;

var remoteDoc;

function onDocLoaded(doc)
{
    setReproStep(3);

    remoteDoc = doc;

    var docModel = doc.getModel();
    var docRoot = docModel.getRoot();

    console.log('Drive Document Loaded');

    // If the loaded document has already been used to test, delete any previous data.
    if (docRoot.has('testMap'))
    {
        console.log('Previous test detected: ' + docRoot.get('testMap').get('testData'));
        docRoot.delete('testMap');
    }

    docRoot.set('testMap', docModel.createMap());

    testMap = docRoot.get('testMap');
    console.assert(testMap, 'Test map required');

    makeDriveDocChange();

    doc.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, onSaveStateChange);

    beginRunningTest();
}

var VerificationTime = 5000;
var ModificationTime = 10000;

function beginRunningTest()
{
    verifyConnectionState();

    setTimeout(setReproStep, ModificationTime * 2, 4);
}

var verificationCount = 0;
function verifyConnectionState()
{
    setTimeout(verifyConnectionState, VerificationTime);

    var saveDelay = remoteDoc.saveDelay;
    var isClosed = remoteDoc.isClosed;
    var lastModification = Date.now() - lastWrite;
    var browserOnline = navigator.onLine;
    var currentCount = ++verificationCount;

    if (!browserOnline && saveDelay > ModificationTime)
    {
        setReproStep(5);

        setTimeout(setReproStep, ModificationTime * 5, 6);
    }

    var isTokenValid = verifyAuthToken();

    verifyDriveConnection(function (driveConnected)
    {
        console.log('--------------------- ' + currentCount + ' ---------------------');

        console.log(' Delay:      ' + saveDelay);
        console.log(' Modified:   ' + lastModification);
        console.log(' IsOnline:   ' + browserOnline);
        console.log(' DriveConn:  ' + driveConnected);
        console.log(' TokenValid: ' + isTokenValid);
        console.log(' IsOpen:     ' + !isClosed);

        if (saveDelay > VerificationTime && driveConnected && !isClosed && browserOnline && isTokenValid)
        {
            console.error('!!! ERROR: Local document not reconnected to remote server. Scenario done.');

            setReproStep(7);
        }
    });

    if (lastModification > ModificationTime && saveDelay === 0)
    {
        makeDriveDocChange();
    }
}

function onSaveStateChange(e)
{

}

function verifyAuthToken()
{
    var isValid = false;

    var token = gapi.auth.getToken();

    if (token)
    {
        var expireTime = parseInt(token.expires_at) * 1000;

        if (Date.now() < expireTime)
        {
            isValid = true;
        }
    }

    return isValid;
}

function makeDriveDocChange()
{
    testMap.set('testData', ++docDataCounter);

    lastWrite = Date.now();
}

function verifyDriveConnection(cb)
{
    gapi.client.drive.about.get({

    }).execute(function (res)
    {
        cb(res && !res.error);
    });
}

function onDocLoadError(e)
{
    console.log('Doc Load Error: ', e);
    findAndLoadDoc();
}

0 个答案:

没有答案