blob: 176e0e0070ddb73566028ec7363853d59622c344 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
require('jasmine-expect');
const net = require('net');
const EventEmitter = require('events');
const Long = require('long');
// Drive the REAL ClientSocket against a mock TCP socket so this regression is
// deterministic and needs no cluster. It reproduces the exact async topology of
// the cold complex-object read: a single socket, one response whose payloadReader
// issues a nested request on that same socket and awaits its reply, where the
// reply only arrives as a later 'data' event. If response finalization is awaited
// on the socket's serialized processing queue, the nested reply is chained behind
// the still-pending outer entry and can never be processed -> the outer request
// hangs forever. With finalization dispatched off the queue, it resolves.
const ClientSocket = require('apache-ignite-client/dist/internal/ClientSocket').default;
const MessageBuffer = require('apache-ignite-client/dist/internal/MessageBuffer').default;
const HANDSHAKE_SUCCESS_STATUS_CODE = 1;
const OP_OUTER = 2001;
const OP_INNER = 2002;
const DEADLOCK_TIMEOUT_MS = 5000;
// Handshake reply frame: [length:int][status:byte]. The >= 1.4.0 path then reads a
// node-UUID via communicator.readObject, which the mock communicator stubs out, so
// no UUID bytes are required (frames are length-delimited, not reader-position
// delimited).
function buildHandshakeResponse() {
const buf = new MessageBuffer();
buf.position = 4;
buf.writeByte(HANDSHAKE_SUCCESS_STATUS_CODE);
const len = buf.length - 4;
buf.position = 0;
buf.writeInteger(len);
return buf.data;
}
// Success response frame for protocol >= 1.4.0: [length:int][requestId:long][flags:short=0].
function buildResponse(requestId) {
const buf = new MessageBuffer();
buf.position = 4;
buf.writeLong(requestId);
buf.writeShort(0);
const len = buf.length - 4;
buf.position = 0;
buf.writeInteger(len);
return buf.data;
}
// Outgoing request frame layout (see ClientSocket Request.getMessage):
// [length:int][opCode:short][requestId:long][payload].
function parseOutgoing(data) {
const buf = MessageBuffer.from(data, 0);
buf.readInteger();
const opCode = buf.readShort();
const requestId = buf.readLong();
return { opCode, requestId };
}
async function withTimeout(promise, ms, message) {
let timer;
const guard = new Promise((resolve, reject) => {
timer = setTimeout(() => reject(new Error(message)), ms);
});
// try/finally rather than Promise.prototype.finally, which is Node 10+
// (package.json declares engines.node >= 8.0.0).
try {
return await Promise.race([promise, guard]);
} finally {
clearTimeout(timer);
}
}
describe('cold complex object read deadlock (mock socket) test suite >', () => {
let origCreateConnection;
beforeEach(() => {
origCreateConnection = net.createConnection;
});
afterEach(() => {
net.createConnection = origCreateConnection;
});
it('response whose payloadReader awaits a nested same-socket request resolves', (done) => {
let clientSocket = null;
Promise.resolve().
then(async () => {
const mockSocket = new EventEmitter();
mockSocket.end = () => {};
let handshakeSent = false;
mockSocket.write = (data) => {
if (!handshakeSent) {
// First write is the handshake (no opCode/id).
handshakeSent = true;
setImmediate(() => mockSocket.emit('data', buildHandshakeResponse()));
return true;
}
// Every subsequent request gets its reply on a LATER event loop
// tick — modelling the real "reply arrives as a future data event"
// ordering, including the nested request the outer reader issues.
const { opCode, requestId } = parseOutgoing(data);
if (opCode === OP_OUTER || opCode === OP_INNER) {
setImmediate(() => mockSocket.emit('data', buildResponse(requestId)));
}
return true;
};
net.createConnection = (options, onConnected) => {
setImmediate(onConnected);
return mockSocket;
};
const config = { options: {}, useTLS: false };
const communicator = { readObject: async () => null };
clientSocket = new ClientSocket(
'127.0.0.1:10800', config, communicator, () => {}, async () => {});
await withTimeout(clientSocket.connect(), DEADLOCK_TIMEOUT_MS,
'handshake did not complete');
let nestedIssued = false;
let nestedResolved = false;
const outer = clientSocket.sendRequest(
OP_OUTER,
async () => {},
async (buffer) => {
// Inline read of the outer response issues a nested request on
// this same socket and awaits it — exactly what readObject does
// for a COMPLEX_OBJECT whose binary type is not yet cached.
nestedIssued = true;
await clientSocket.sendRequest(OP_INNER, async () => {}, async () => {});
nestedResolved = true;
});
await withTimeout(outer, DEADLOCK_TIMEOUT_MS,
'outer request did not resolve within ' + DEADLOCK_TIMEOUT_MS +
'ms — the response processing queue deadlocked on the nested ' +
'same-socket request');
expect(nestedIssued).toBe(true);
expect(nestedResolved).toBe(true);
}).
then(done).
catch(error => done.fail(error)).
then(() => {
if (clientSocket) {
try { clientSocket.disconnect(); } catch (_e) { /* ignore */ }
}
});
});
});