В первой части мы разбирались, как происходит получение одной строки из таблицы базы данных. Сегодня попробуем понять, что с этой строкой происходит дальше.

RecordSource

Поищем, кто вызывает VIO_get(), и сразу находим два очень любопытных места: BitmapTableScan::internalGetRecord() и IndexTableScan::internalGetRecord(), обе из файлов, которые расположены в /src/jrd/recsrc/ . Я назову эту подсистему "Record Source", но ещё я встречал название "Методы доступа/Access methods" вот в этой очень неплохой статье. Раскручивая цепочки вызовов вышеупомянутых функций, мы сталкиваемся с одной проблемой: классы BitmapTableScan и IndexTableScan являются потомками RecordStream, который является потомком RecordSource, который является потомком AccessPath. То есть у нас тут иерархия классов, маленький framework. В VSCode я не знаю способа, как искать, кто вызывает методы, тут есть только "Find references", который ищет и реализации метода в классах-потомках. Через анализ исходников вырваться из фреймворка весьма непросто.

Debugger

Что же, опять меняем подход со статического на динамический. У нас есть ещё приём, который мы пока не применяли: отладка! Я буду отлаживаться из VSCode c плагином поддержки C/C++. Кто-то из авторов тоже пользуется VSCode, и даже добавил каталог ".vscode" в git-репозиторий, не надо так делать. Выбираю в меню "Run"/"Open configurations", стираю конфигурации авторов, и добавляю свою, вот такую:

"configurations": [
	{
		"name": "(Windows) Attach",
		"type": "cppvsdbg",
		"request": "attach",
		"program": "C:\\tools\\firebird\\firebird.exe",
		"symbolSearchPath": "C:\\tools\\firebird\\;C:\\tools\\firebird\\plugins\\",
		"requireExactSource": true
	},
]

Как вы, наверное, догадались, я собираюсь совсем пропустить этап сборки Firebird из исходников, вместо этого считаю, что бинарник из дистрибутива собран из тех исходников, которые я скачал по git-тегу. Это позволит мне прицепляться отладчиком к процессу, запущенному из дистрибутива. Хорошая новость: это работает, VSCode предлагает выбрать процесс для прицепляния, выбираем firebird.exe . Есть только маленькое неудобство: ставлю я точку останова, например, в VIO_data(), и эта точка срабатывает, но VSCode говорит: не могу найти файл "c:\firebird-build\src\jrd\vio.cpp". Ну конечно он не может, по этому пути лежали исходники на той build-машине, где Firebird собирали в дистрибутив, и этот путь в pdb-файле сохранился, а у меня исходники в другом месте лежат. Я не знаю, как это победить, поэтому поместил исходники туда же, где они лежали на build-машине. Теперь всё.

Ставим точку останова в VIO_data(), делаем тупой запрос "SELECT * FROM ORDERS", точка останова срабатывает и мы получаем очень полезный stacktrace:

engine13.dll!VIO_data()
engine13.dll!VIO_next_record()
engine13.dll!Jrd::FullTableScan::internalGetRecord()
engine13.dll!Jrd::RecordSource::getRecord()
engine13.dll!Jrd::Cursor::fetchNext()
engine13.dll!Jrd::ForNode::execute()
engine13.dll!EXE_looper()
engine13.dll!looper_seh()
engine13.dll!execute_looper()
engine13.dll!EXE_receive()
engine13.dll!JRD_receive()
engine13.dll!Jrd::DsqlDmlRequest::executeReceiveWithRestarts()
engine13.dll!Jrd::DsqlDmlRequest::fetch()
engine13.dll!Jrd::DsqlCursor::fetchNext()
engine13.dll!Jrd::JResultSet::fetchNext()
engine13.dll!Firebird::IResultSetBaseImpl::cloopfetchNextDispatcher()
[Inline Frame] fbclient.dll!Firebird::IResultSet::fetchNext()
fbclient.dll!Why::YResultSet::fetchNext()
fbclient.dll!Firebird::IResultSetBaseImpl::cloopfetchNextDispatcher()
[Inline Frame] firebird.exe!Firebird::IResultSet::fetchNext()
firebird.exe!rem_port::fetch()
firebird.exe!process_packet()
firebird.exe!loopThread()
[Inline Frame] firebird.exe!`anonymous-namespace'::ThreadArgs::run()
firebird.exe!threadStart()
ucrtbase.dll!00007ffc494a1bb2()
kernel32.dll!00007ffc4b4a7374()
ntdll.dll!00007ffc4b93cc91()

Что же тут интересного? Во-первых, функция VIO_data() вызвана не из VIO_get(), а из VIO_next_record(), которую мы ещё не рассматривали. А вот она уже вызывается из FullTableScan::internalGetRecord(), это ещё один метод доступа к строкам таблицы, смысл которого в том, чтобы тупо проходиться по всем строкам таблицы подряд. Он применяется, например, тогда, когда мы не указали ни одного условия фильтрации строк в запросе в секции WHERE, как раз наш случай. Дальше по стектрейсу мы видим функции из фреймворка RecordSource. Пропустим несколько строк, которые нам сейчас ни о чём не скажут, и видим Jrd::JResultSet::fetchNext(), это похоже на API-вызов. Дальше идут разные технические функции, завершает которые функция process_packet() из файла "server.cpp". Возможно, вы помните, что в предыдущей статье я говорил, что сервер граничит с двумя вещами: сеть с одной стороны, диск с другой. Так вот, process_packet() - это обработчик сетевого протокола. Там внутри гигантский switch по типам операций. Поэтому очень хорошо, что я не начал раскопки оттуда. Но если вам интересно устройство сетевого протокола Firebird, то это как раз то место, где нужно начать. От себя добавлю только, что с упрощённым динамическим исследованием этого слоя всё плохо: никакого логирования входящих пакетов нет и включить нельзя.

Ранее, когда мы анализировали исходный код, мы шли от вызываемых функций к вызывающим, так сказать "снизу вверх". В отладке так не очень удобно: точка останова помогла нам понять цепочку вызовов, но хочется отладиться в пошаговом режиме. Поэтому я сделаю так: сразу заберусь повыше, в JResultSet::fetchNext(), и буду спускаться оттуда. А stacktrace будет меня направлять. В этой части мы пройдём по примерно половине из функций, отмеченных в stacktrace.

Сам JResultSet какой-то никакой: он содержит ссылку на JStatement, выполнение которого породило этот JResultSet, и DslCursor, который делает всю работу. Метод JResultSet::fetchNext() делегирует всю работу, вызывая DslCursor::fetchNext(), что мы и видели в stacktrace.

Dsql@1

Класс DslCursor тоже не особо большой. Тут есть кеширование и управление движением по записям, но в нашем простом запросе это не используется (я обещал придерживаться core-функциональности). Метод DslCursor::fetchNext() делегирует свою работу методу DsqlDmlRequest::fetch().

Метод DsqlDmlRequest::fetch() уже сильно интереснее.

// Fetch next record from a dynamic SQL cursor.
bool DsqlDmlRequest::fetch(thread_db* tdbb, UCHAR* msgBuffer)
{
	SET_TDBB(tdbb);

	Jrd::ContextPoolHolder context(tdbb, &getPool());

	// if the cursor isn't open, we've got a problem
	if (dsqlStatement->isCursorBased()) {
		if (!req_cursor) {
			ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-504) <<
					  Arg::Gds(isc_dsql_cursor_err) <<
					  Arg::Gds(isc_dsql_cursor_not_open));
		}
	}

	if (!request) {
		ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-504) <<
				  Arg::Gds(isc_unprepared_stmt));
	}

	dsql_msg* message = (dsql_msg*) dsqlStatement->getReceiveMsg();

	if (delayedFormat && message) {
		parseMetadata(delayedFormat, message->msg_parameters);
		delayedFormat = NULL;
	}

	// Set up things for tracing this call
	Jrd::Attachment* att = req_dbb->dbb_attachment;
	TraceDSQLFetch trace(att, this);

	thread_db::TimerGuard timerGuard(tdbb, req_timer, false);
	if (req_timer && req_timer->expired())
		tdbb->checkCancelState();

	UCHAR* dsqlMsgBuffer = req_msg_buffers[message->msg_buffer_number];
	if (!firstRowFetched && needRestarts()) {
		// Note: tra_handle can't be changed by executeReceiveWithRestarts below
		// and outMetadata and outMsg in not used there, so passing NULL's is safe.
		jrd_tra* tra = req_transaction;

		executeReceiveWithRestarts(tdbb, &tra, NULL, NULL, false, false, true);
	}
	else
		JRD_receive(tdbb, request, message->msg_number, message->msg_length, dsqlMsgBuffer);

	firstRowFetched = true;

	const dsql_par* const eof = dsqlStatement->getEof();
	const USHORT* eofPtr = eof ? (USHORT*) (dsqlMsgBuffer + (IPTR) eof->par_desc.dsc_address) : NULL;
	const bool eofReached = eof && !(*eofPtr);

	if (eofReached) {
		if (req_timer)
			req_timer->stop();

		trace.fetch(true, ITracePlugin::RESULT_SUCCESS);
		return false;
	}

	if (msgBuffer) {
		Request* old = tdbb->getRequest();
		Cleanup restoreRequest([tdbb, old] {tdbb->setRequest(old);});
		tdbb->setRequest(request);
		mapInOut(tdbb, true, message, NULL, msgBuffer);
	}

	trace.fetch(false, ITracePlugin::RESULT_SUCCESS);
	return true;
}

Этот метод вызывает несколько вспомогательных методов, в которые мы будем заглядывать и снова возвращаться к этому методу. Пропускаем всякие проверки в начале, добираемся до места "dsqlStatement->getReceiveMsg()", и чешем затылок в недоумении: какой ещё receiveMsg ? Как хорошо, что у нас есть отладчик, мы можем выполнить эту строку и посмотреть, что мы получим. Будет какая-то структура с массивом на 11 элементов внутри. Оно передаётся в метод parseMetadata() вместе с полем delayedFormat. Поле delayedFormat - это экземпляр какого-то страшного класса, который является частью API, через который вызывающий сообщает, что он хочет получить. Присваивается это поле тоже через API, методом JResultSet::setDelayedFormat(). Посмотрим, что делает DsqlRequest::parseMetadata().

// Parse the message of a request.
USHORT DsqlRequest::parseMetadata(IMessageMetadata* meta, const Array<dsql_par*>& parameters_list)
{
	HalfStaticArray<const dsql_par*, 16> parameters;

	for (FB_SIZE_T i = 0; i < parameters_list.getCount(); ++i) {
		dsql_par* param = parameters_list[i];

		if (param->par_index) {
			if (param->par_index > parameters.getCount())
				parameters.grow(param->par_index);
			parameters[param->par_index - 1] = param;
		}
	}

	// If there's no metadata, then the format of the current message buffer
	// is identical to the format of the previous one.
	if (!meta)
		return parameters.getCount();

	FbLocalStatus st;
	unsigned count = meta->getCount(&st);

	unsigned count2 = parameters.getCount();

	if (count != count2) {
		ERRD_post(Arg::Gds(isc_dsql_sqlda_err) <<
				  Arg::Gds(isc_dsql_wrong_param_num) <<Arg::Num(count2) << Arg::Num(count));
	}

	unsigned offset = 0;

	for (USHORT index = 0; index < count; index++) {
		unsigned sqlType = meta->getType(&st, index);
		unsigned sqlLength = meta->getLength(&st, index);

		dsc desc;
		desc.dsc_flags = 0;

		unsigned dataOffset, nullOffset, dtype, dlength;
		offset = fb_utils::sqlTypeToDsc(offset, sqlType, sqlLength,
			&dtype, &dlength, &dataOffset, &nullOffset);
		desc.dsc_dtype = dtype;
		desc.dsc_length = dlength;

		desc.dsc_scale = meta->getScale(&st, index);
		desc.dsc_sub_type = meta->getSubType(&st, index);
		unsigned textType = meta->getCharSet(&st, index);
		desc.setTextType(textType);
		desc.dsc_address = (UCHAR*)(IPTR) dataOffset;

		const dsql_par* const parameter = parameters[index];

		// ASF: Older than 2.5 engine hasn't validating strings in DSQL. After this has been
		// implemented in 2.5, selecting a NONE column with UTF-8 attachment charset started
		// failing. The real problem is that the client encodes SQL_TEXT/SQL_VARYING using
		// blr_text/blr_varying (i.e. with the connection charset). I'm reseting the charset
		// here at the server as a way to make older (and not yet changed) client work
		// correctly.
		if (desc.isText() && desc.getTextType() == ttype_dynamic)
			desc.setTextType(ttype_none);

		req_user_descs.put(parameter, desc);

		dsql_par* null = parameter->par_null;
		if (null) {
			desc.clear();
			desc.dsc_dtype = dtype_short;
			desc.dsc_scale = 0;
			desc.dsc_length = sizeof(SSHORT);
			desc.dsc_address = (UCHAR*)(IPTR) nullOffset;

			req_user_descs.put(null, desc);
		}
	}

	return count;
}

typedef struct dsc {
	UCHAR	dsc_dtype;
	SCHAR	dsc_scale;
	USHORT	dsc_length;
	SSHORT	dsc_sub_type;
	USHORT	dsc_flags;
	UCHAR*	dsc_address; // Used either as offset in a message or as a pointer
}

Вначале идёт цикл, который перекладывает в локальный список-массив те элементы из входящего списка параметров, у которых есть индекс par_index. Зачем это нужно, и для какой цели применяются остальные параметры, у которых индекса нет - непонятно. Далее проверяется, что количество параметров в meta и parameters совпадает. После чего начинается главное: цикл заполнения информации о параметрах на основе того, что хранится в meta. Информация о параметрах сохраняется в структуры dsc. Так-так, знакомое название, в первой статье была структура Ods::Descriptor , один-в-один. Цикл идёт по последовательности параметров, как будто это строка таблицы, в которой поля лежат друг за другом, и расcчитывает смещение dsc_address каждого параметра в этой последовательности. В завершение описатели параметров помещаются в мапу req_user_desc.

Возвращаемся в fetch(). Итак, parseMetadata() скрестила информацию из receivedMsg и delayedFormat, сформировав описание того, как параметры можно сохранить в едином буфере и поместив это в req_user_desc. Эта операция делается один раз, после первого выполнения поле delayedFormat обнуляется и при последующих вызовах fetch() будет использоваться уже подготовленный req_user_desc.

Спустимся чуть ниже и видим if, в котором в зависимости от того, была ли уже извлечена первая строка или нет, происходит переход на executeReceiveWithRestarts() или JRD_Receive(). Мы пойдём в первый из них, но перед этим задержимся ещё чуть-чуть в этой функции. Тут есть TraceDsqlFetch, и если мы в него чуть углубимся, то попадём в TraceManager::event_dsql_execute() . Приглядевшись повнимательнее, мы поймём, что это тот самый TraceManager, который писал трассировку из первой части статьи. В файле "TraceManager.h" у класса TraceManager определены методы типа "event_proc_compile", которые соответствуют событиям, которые можно получать. Так что список событий - он вот тут. Немного смущает, что event_dsql_prepare() описан как static и я не смог его трассировать, ну да ладно, как-нибудь потом разберусь.

Итак, если первая строка ещё не была извлечена, то вызывается метод executeReceiveWithRestarts(), причём аргумент exec будет равен false, а аргумент fetch будет равен true. Что же, глянем на него.

void DsqlDmlRequest::executeReceiveWithRestarts(thread_db* tdbb, jrd_tra** traHandle,
	IMessageMetadata* outMetadata, UCHAR* outMsg,
	bool singleton, bool exec, bool fetch)
{
	request->req_flags &= ~req_update_conflict;
	int numTries = 0;
	const int MAX_RESTARTS = 10;

	while (true) {
		AutoSavePoint savePoint(tdbb, req_transaction);

		// Don't set req_restart_ready flag at last attempt to restart request.
		// It allows to raise update conflict error (if any) as usual and
		// handle error by PSQL handler.
		const ULONG flag = (numTries >= MAX_RESTARTS) ? 0 : req_restart_ready;
		AutoSetRestoreFlag<ULONG> restartReady(&request->req_flags, flag, true);
		try
		{
			if (exec)
				doExecute(tdbb, traHandle, outMetadata, outMsg, singleton);

			if (fetch) {
				const dsql_msg* message = dsqlStatement->getReceiveMsg();
				UCHAR* dsqlMsgBuffer = req_msg_buffers[message->msg_buffer_number];
				JRD_receive(tdbb, request, message->msg_number, message->msg_length, dsqlMsgBuffer);
			}
		}
		catch (const status_exception&) {
			if (!(req_transaction->tra_flags & TRA_ex_restart)) {
				request->req_flags &= ~req_update_conflict;
				throw;
			}
		}

		if (!(request->req_flags & req_update_conflict)) {
			req_transaction->tra_flags &= ~TRA_ex_restart;
			savePoint.release();	// everything is ok
			break;
		}

		request->req_flags &= ~req_update_conflict;
		req_transaction->tra_flags &= ~TRA_ex_restart;
		fb_utils::init_status(tdbb->tdbb_status_vector);

		// Undo current savepoint but preserve already taken locks.
		// Savepoint will be restarted at the next loop iteration.
		savePoint.rollback(true);

		numTries++;
		if (numTries >= MAX_RESTARTS){
			gds__log("Update conflict: unable to get a stable set of rows in the source tables\n"
				"\tafter %d attempts of restart.\n"
				"\tQuery:\n%s\n", numTries, request->getStatement()->sqlText->c_str() );
		}

		TraceManager::event_dsql_restart(req_dbb->dbb_attachment, req_transaction, this, numTries);

		// When restart we must execute query
		exec = true;
	}
}

Суть этой функции состоит в наличии цикла while, внутри которого в рамках текущей транзакции создаётся savepoint, после чего происходит попытка выполнить действия doExecute() и JRD_Receive(). Если было выброшено С++ исключение, то оно ловится и проверяется флаг TRA_ex_restart, который указывает, что действие конфликтнуло с другой транзакцией, но его можно попробовать повторить. Если флаг выставлен, то savepoint откатывается, и происходит повтор, для которого будет создан новый savepoint, оптимизм. Я не уверен на 100%, но мне сильно кажется, что для нашего SELECT-запроса вся эта логика не особо нужна, поскольку он не обновляет данные, только читает, поэтому создание и отпускание savepoint-а являются излишеством. Попозже посмотрю, насколько это дорогая операция. С одной стороны мы помним, что эта работа с savepoint-ами происходит только при чтении первой строки, для остальных строк она не нужна, уже неплохо. С другой стороны я начинаю понимать, почему рекомендуют не писать подзапросы и заменять их на JOIN-ы.

Итак, метод executeReceiveWithRestarts() вызывается при извлечении первой строки и для выполнения всей работы вызовет JRD_Receive(). Как мы помним, в вызывающей функции fetch() он же ( JRD_Receive() ) будет вызван напрямую для всех последующих строк таблицы. Отметим только, что в качестве одного из аргументов туда передаётся некий request, который в DsqlDmlRequest хранится в приватном поле. А в качестве остальных параметров мы передаём три свойства объекта dsqlStatement->message, а именно: его номер msg_number, его длину msg_length и адрес выделенного буфера.

Exe@1

Функция JRD_Receive() очень маленькая, тут даже объяснять нечего.

void JRD_receive(thread_db* tdbb, Request* request, USHORT msg_type, ULONG msg_length, void* msg)
{
 **************************************
 * Functional description
 *	Get a record from the host program.
 **************************************/
	EXE_receive(tdbb, request, msg_type, msg_length, msg, true);

	check_autocommit(tdbb, request);

	if (request->req_flags & req_warning) {
		request->req_flags &= ~req_warning;
		ERR_punt();
	}
}

Что имелось в виду под "host program" - неясно.

Просто идём в EXE_receive(), она большая. Но не нужно пугаться, большая часть здешних if-ов не будет выполняться в нашем случае.

void EXE_receive(thread_db* tdbb,
				 Request* request,
				 USHORT msg,
				 ULONG length,
				 void* buffer,
				 bool top_level)
{
	SET_TDBB(tdbb);
	DEV_BLKCHK(request, type_req);
	JRD_reschedule(tdbb);

	jrd_tra* transaction = request->req_transaction;

	if (!(request->req_flags & req_active))
		ERR_post(Arg::Gds(isc_req_sync));

	SavNumber savNumber = 0;

	if (request->req_flags & req_proc_fetch) {
		/* request->req_proc_sav_point stores the request savepoints.
		   When going to continue execution put request save point list
		   into transaction->tra_save_point so that it is used in looper.
		   When we come back to EXE_receive() merge all work done under
		   stored procedure savepoints into the current transaction
		   savepoint, which is the savepoint for fetch and save them into the list. */

		if (request->req_proc_sav_point) {
			// We assume here that the saved savepoint stack starts with the
			// smallest number, the logic will be broken if this ever changes
			savNumber = request->req_proc_sav_point->getNumber();
			// Push all saved savepoints to the top of transaction savepoints stack
			Savepoint::mergeStacks(transaction->tra_save_point, request->req_proc_sav_point);
		} else 	{
			const auto savepoint = transaction->startSavepoint();
			savNumber = savepoint->getNumber();
		}
	}

	try {
		if (nodeIs<StallNode>(request->req_message))
			execute_looper(tdbb, request, transaction, request->req_next, Request::req_sync);

		if (!(request->req_flags & req_active) || request->req_operation != Request::req_send)
			ERR_post(Arg::Gds(isc_req_sync));

		const MessageNode* message = nodeAs<MessageNode>(request->req_message);
		const Format* format = message->format;

		if (msg != message->messageNumber)
			ERR_post(Arg::Gds(isc_req_sync));

		if (length != format->fmt_length)
			ERR_post(Arg::Gds(isc_port_len) << Arg::Num(length) << Arg::Num(format->fmt_length));

		memcpy(buffer, request->getImpure<UCHAR>(message->impureOffset), length);

		// ASF: temporary blobs returned to the client should not be released
		// with the request, but in the transaction end.
		if (top_level || transaction->tra_temp_blobs_count) {
			for (int i = 0; i < format->fmt_count; ++i) {
				const DSC* desc = &format->fmt_desc[i];

				if (desc->isBlob()) {
					const bid* id = (bid*) (static_cast<UCHAR*>(buffer) + (ULONG)(IPTR) desc->dsc_address);

					if (transaction->tra_blobs->locate(id->bid_temp_id())) {
						BlobIndex* current = &transaction->tra_blobs->current();

						if (top_level &&
							current->bli_request &&
							current->bli_request->req_blobs.locate(id->bid_temp_id()))
						{
							current->bli_request->req_blobs.fastRemove();
							current->bli_request = NULL;
						}

						if (!current->bli_materialized &&
							(current->bli_blob_object->blb_flags & (BLB_close_on_read | BLB_stream)) ==
								(BLB_close_on_read | BLB_stream))
						{
							current->bli_blob_object->BLB_close(tdbb);
						}
					} else if (top_level) {
						transaction->checkBlob(tdbb, id, NULL, false);
					}
				}
			}
		}

		execute_looper(tdbb, request, transaction, request->req_next, Request::req_proceed);
	}
	catch (const Exception&) {
		// In the case of error, undo changes performed under our savepoint

		if (savNumber)
			transaction->rollbackToSavepoint(tdbb, savNumber);

		throw;
	}

	if (savNumber) {
		// At this point request->req_proc_sav_point == NULL that is assured by code above
		try {
			// Merge work into target savepoint and save request's savepoints (with numbers!!!)
			// till the next looper iteration
			while (transaction->tra_save_point &&
				transaction->tra_save_point->getNumber() >= savNumber)
			{
				const auto savepoint = transaction->tra_save_point;
				transaction->releaseSavepoint(tdbb);
				transaction->tra_save_free = savepoint->moveToStack(request->req_proc_sav_point);

				// Ensure that the priorly existing savepoints are preserved,
				// e.g. 10-11-12-(5-6-7) where savNumber == 5. This may happen
				// due to looper savepoints being reused in subsequent invokations.
				if (savepoint->getNumber() == savNumber)
					break;
			}
		}
		catch (...) {
			// If something went wrong, drop already stored savepoints to prevent memory leak
			Savepoint::destroy(request->req_proc_sav_point);
			throw;
		}
	}
}

Первый из не-тривиальных if-ов проверяет, нет ли на request-е флага req_proc_fetch, который, судя по названию, выставляется только в хранимых процедурах, поэтому пропускаем этот if, отладчик в него тоже не зайдёт.

А вот проверка, что request->req_message является неким StallNode, пройдёт, и будет вызван execute_looper(). Тормознём на секундочку и посмотрим в отладчике, что у нас за значения. В request->req_message действительно хранится экземпляр Jrd::StallNode. Также заметим, что в execute_looper(), помимо самого request-а, передаются request->req_next (в параметр node) и Request::req_sync (в параметр next_state). В отладчике отлично видно, что request->req_message и request->req_next указывают на один и тот же объект, и я проверил, что это не только в случае чтения самой первой строки.

Функция execute_looper() очень важная и большая, и позже мы её обязательно рассмотрим, но сейчас лучше будем считать её чёрным ящиком и продолжим рассмотрение EXE_receive().

Сразу после возвращения из execute_looper() проверяется, что у Request-а снят флаг req_active и операция req_operation выставлена в req_send, в противном случае сильно ругаемся. Дальше забираем req_message и ожидаем, что он будет являться экземпляром MessageNode. Ага, то есть до вызова execute_looper() там был StallNode, но после выполнения имеем MessageNode, видимо это какое-то сообщение с данными, которое нужно отправить тому, кто сделал fetch(). Следующим шагом из MessageNode забирается уже знакомая нам штука Format, который, как мы видим, используется не только для описания того, как хранятся данные на диске, но и то, как передаются данные по сети. Дальше проверяется, что полученный messageNumber соответствует ожидаемому, и что размер данных не превысил ожидаемый. Если всё хорошо, то копируем данные из MessageNode в предоставленный буфер.

Дальше идёт проверка, что временные блобы нужно освободить, это не интересно. После чего execute_looper() вызывается второй раз, уже с параметром Request::req_proceed, интересно. Похоже, что это какая-то пост-обработка запроса, когда возвращаемые значения уже получены.

Хвост функции, обрабатывающий транзакции и savepoint-ы, мне не очень понятен, его я пропущу.

Отложим погружение в execute_looper() ещё ненадолго, и продолжим всплытие: посмотрим, что будет происходить после возвращения из EXE_receive().

Dsql@2

Цепочка возвратов приведёт нас к самому первом блоку кода в этой части, к методу DsqlDmlRequest::fetch(), выполнение которого продолжится сразу после возврата из executeReceiveWithRestarts()/JRD_receive() . После проверки на end-of-file там вызывается mapInOut()

// Map data from external world into message or from message to external world.
void DsqlDmlRequest::mapInOut(thread_db* tdbb, bool toExternal, const dsql_msg* message,
	IMessageMetadata* meta, UCHAR* dsql_msg_buf, const UCHAR* in_dsql_msg_buf)
{
	USHORT count = parseMetadata(meta, message->msg_parameters);

	USHORT count2 = 0;

	for (FB_SIZE_T i = 0; i < message->msg_parameters.getCount(); ++i) {
		dsql_par* parameter = message->msg_parameters[i];

		if (parameter->par_index) {
			 // Make sure the message given to us is long enough

			dsc desc;
			if (!req_user_descs.get(parameter, desc))
				desc.clear();

			UCHAR* msgBuffer = req_msg_buffers[parameter->par_message->msg_buffer_number];

			SSHORT* flag = NULL;
			dsql_par* const null_ind = parameter->par_null;
			if (null_ind != NULL) {
				dsc userNullDesc;
				if (!req_user_descs.get(null_ind, userNullDesc))
					userNullDesc.clear();

				const ULONG null_offset = (IPTR) userNullDesc.dsc_address;


				dsc nullDesc = null_ind->par_desc;
				nullDesc.dsc_address = msgBuffer + (IPTR) nullDesc.dsc_address;

				if (toExternal) {
					flag = reinterpret_cast<SSHORT*>(dsql_msg_buf + null_offset);
					*flag = *reinterpret_cast<const SSHORT*>(nullDesc.dsc_address);
				} else {
					flag = reinterpret_cast<SSHORT*>(nullDesc.dsc_address);
					*flag = *reinterpret_cast<const SSHORT*>(in_dsql_msg_buf + null_offset);
				}
			}

			const bool notNull = (!flag || *flag >= 0);

			dsc parDesc = parameter->par_desc;
			parDesc.dsc_address = msgBuffer + (IPTR) parDesc.dsc_address;

			if (toExternal) {
				desc.dsc_address = dsql_msg_buf + (IPTR) desc.dsc_address;

				if (notNull)
					MOVD_move(tdbb, &parDesc, &desc);
				else
					memset(desc.dsc_address, 0, desc.dsc_length);
			} else if (notNull && !parDesc.isNull()) {
				// Safe cast because desc is used as source only.
				desc.dsc_address = const_cast<UCHAR*>(in_dsql_msg_buf) + (IPTR) desc.dsc_address;
				MOVD_move(tdbb, &desc, &parDesc);
			}
			else
				memset(parDesc.dsc_address, 0, parDesc.dsc_length);

			++count2;
		}
	}

	if (count != count2) {
		ERRD_post(
			Arg::Gds(isc_dsql_sqlda_err) <<
			Arg::Gds(isc_dsql_wrong_param_num) << Arg::Num(count) <<Arg::Num(count2));
	}

	const auto dsqlStatement = getDsqlStatement();
	const dsql_par* parameter;

	const dsql_par* dbkey;
	if (!toExternal && (dbkey = dsqlStatement->getParentDbKey()) &&
		(parameter = dsqlStatement->getDbKey()))
	{
		UCHAR* parentMsgBuffer = dsqlStatement->getParentRequest() ?
			dsqlStatement->getParentRequest()->req_msg_buffers[dbkey->par_message->msg_buffer_number] : NULL;
		UCHAR* msgBuffer = req_msg_buffers[parameter->par_message->msg_buffer_number];

		dsc parentDesc = dbkey->par_desc;
		parentDesc.dsc_address = parentMsgBuffer + (IPTR) parentDesc.dsc_address;

		dsc desc = parameter->par_desc;
		desc.dsc_address = msgBuffer + (IPTR) desc.dsc_address;

		MOVD_move(tdbb, &parentDesc, &desc);

		dsql_par* null_ind = parameter->par_null;
		if (null_ind != NULL) {
			desc = null_ind->par_desc;
			desc.dsc_address = msgBuffer + (IPTR) desc.dsc_address;

			SSHORT* flag = (SSHORT*) desc.dsc_address;
			*flag = 0;
		}
	}

	const dsql_par* rec_version;
	if (!toExternal && (rec_version = dsqlStatement->getParentRecVersion()) &&
		(parameter = dsqlStatement->getRecVersion()))
	{
		UCHAR* parentMsgBuffer = dsqlStatement->getParentRequest() ?
			dsqlStatement->getParentRequest()->req_msg_buffers[rec_version->par_message->msg_buffer_number] :
			NULL;
		UCHAR* msgBuffer = req_msg_buffers[parameter->par_message->msg_buffer_number];

		dsc parentDesc = rec_version->par_desc;
		parentDesc.dsc_address = parentMsgBuffer + (IPTR) parentDesc.dsc_address;

		dsc desc = parameter->par_desc;
		desc.dsc_address = msgBuffer + (IPTR) desc.dsc_address;

		MOVD_move(tdbb, &parentDesc, &desc);

		dsql_par* null_ind = parameter->par_null;
		if (null_ind != NULL) {
			desc = null_ind->par_desc;
			desc.dsc_address = msgBuffer + (IPTR) desc.dsc_address;

			SSHORT* flag = (SSHORT*) desc.dsc_address;
			*flag = 0;
		}
	}
}

В этом методе выполняется цикл по параметрам message->msg_parameters. Для каждого параметра используются два дескриптора: пользовательский дескриптор desc , который забирается из мапы req_user_descs, и исходящий дескриптор parameter->par_desc . Функция MOVD_move() используется, чтобы скопировать данные из пользовательского дескриптора в исходящий. Два больших if-а во второй части метода обрабатывают особые случаи, и нам не интересны.

Exe@2

Пришло время вернуться к рассмотрению функции execute_looper().

static void execute_looper(thread_db* tdbb,
						   Request* request,
						   jrd_tra* transaction,
						   const StmtNode* node,
						   Request::req_s next_state)
{
	DEV_BLKCHK(request, type_req);

	SET_TDBB(tdbb);
	Jrd::Attachment* const attachment = tdbb->getAttachment();

	// Ensure the cancellation lock can be triggered

	Lock* const lock = attachment->att_cancel_lock;
	if (lock && lock->lck_logical == LCK_none)
		LCK_lock(tdbb, lock, LCK_SR, LCK_WAIT);

	// Start a save point

	SavNumber savNumber = 0;

	if (!(request->req_flags & req_proc_fetch) && request->req_transaction) {
		if (transaction && !(transaction->tra_flags & TRA_system)) {
			if (request->req_savepoints) {
				request->req_savepoints =
					request->req_savepoints->moveToStack(transaction->tra_save_point);
			}
			else
				transaction->startSavepoint();

			savNumber = transaction->tra_save_point->getNumber();
		}
	}

	request->req_flags &= ~req_stall;
	request->req_operation = next_state;

	try {
		looper_seh(tdbb, request, node);
	} 
	catch (const Exception&) {
		// In the case of error, undo changes performed under our savepoint
		if (savNumber)
			transaction->rollbackToSavepoint(tdbb, savNumber);

		throw;
	}

	// If any requested modify/delete/insert ops have completed, forget them

	if (savNumber) {

		while (transaction->tra_save_point &&
			transaction->tra_save_point->getNumber() >= savNumber) {
			const auto savepoint = transaction->tra_save_point;
			// Forget about any undo for this verb
			transaction->releaseSavepoint(tdbb);
			// Preserve savepoint for reuse
			transaction->tra_save_free = savepoint->moveToStack(request->req_savepoints);

			// Ensure that the priorly existing savepoints are preserved,
			// e.g. 10-11-12-(5-6-7) where savNumber == 5. This may happen
			// due to looper savepoints being reused in subsequent invokations.
			if (savepoint->getNumber() == savNumber)
				break;
		}
	}
}

В первой части этой функции опять логика работы с savepoint-ами. Отладчик показывает, что request->req_savepoints не пустой (ещё бы, 3 функции назад добавляли), и moveToStack() будет вызван, но это вроде как не особо важно для нас, у нас SELECT.

Главная часть функции тупая, как я: снять флаг req_stall, сохранить параметр next_state в request->req_operation, и потом в try/catch блоке вызвать looper_seh(), поймать исключение. Так, минуточку, где-то чуть выше уже была ловля исключения, здесь-то она зачем? А дело вот в чём: тут мы ловим все исключения, откатываем состояние транзакции и бросаем исключение дальше. А в предыдущем случае мы ловили только исключения, которые намекают, что операцию можно повторить, и повторяли. Это два принципиально разных поведения: там была окончательная ловля исключения с целью активного решения проблемы, а тут - промежуточная ловля с целью поддержания корректного состояния. Ну и завершающая часть функции - отпускание savepoint-а, если всё хорошо. Эх, когда уже закончится вся эта обвязочная логика обработки ошибок, и мы доберёмся до чтения данных?

Функция looper_seh() - это какая-то очень техническая обвязка для поддержки Structured Exception Handling в Windows.

// Start looper under Windows SEH (Structured Exception Handling) control
static void looper_seh(thread_db* tdbb, Request* request, const StmtNode* node)
{
#ifdef WIN_NT
	START_CHECK_FOR_EXCEPTIONS(NULL);
#endif
	// TODO:
	// 1. Try to fix the problem with MSVC C++ runtime library, making
	// even C++ exceptions that are implemented in terms of Win32 SEH
	// getting catched by the SEH handler below.
	// 2. Check if it really is correct that only Win32 catches CPU
	// exceptions (such as SEH) here. Shouldn't any platform capable
	// of handling signals use this stuff?
	// (see jrd/ibsetjmp.h for implementation of these macros)

	EXE_looper(tdbb, request, node);

#ifdef WIN_NT
	END_CHECK_FOR_EXCEPTIONS(NULL);
#endif
}

Тут я не спец, прокомментировать не смогу. Просто идём в EXE_looper().

Мы уже в двух шагах от RecordSource-фреймворка, и важная логика должна быть где-то тут. Функцию EXE_looper() я для наглядности порезал по количеству строк довольно сильно. Дело в том, что в релизе 5.0 в Firebird была добавлена функциональность профилирования запросов (статья на Хабре), и функция EXE_looper() содержит немалое количество кода для поддержки профилирования: проверки, нужно профилировать или нет, подсчёт количества тиков, и прочее. Я даже думаю, что это и есть главное место в коде, где реализовано профилирование. Но для нашего понимания логики выполнения запросов этот код профилирования только мешает, помним: сосредоточились на core-функциональности. После очистки функция стала вот такая:

const StmtNode* EXE_looper(thread_db* tdbb, Request* request, const StmtNode* node)
{
	if (!request->req_transaction)
		ERR_post(Arg::Gds(isc_req_no_trans));

	SET_TDBB(tdbb);
	const auto dbb = tdbb->getDatabase();
	const auto attachment = tdbb->getAttachment();

	if (!node)
		BUGCHECK(147);

	// Save the old pool and request to restore on exit
	StmtNode::ExeState exeState(tdbb, request, request->req_transaction);
	Jrd::ContextPoolHolder context(tdbb, request->req_pool);

	request->req_caller = exeState.oldRequest;

	tdbb->tdbb_flags &= ~(TDBB_stack_trace_done | TDBB_sys_error);

	// Execute stuff until we drop
	while (node && !(request->req_flags & req_stall)) {
		try {
			if (request->req_operation == Request::req_evaluate) {
				JRD_reschedule(tdbb);
				if (node->hasLineColumn) {
					request->req_src_line = node->line;
					request->req_src_column = node->column;
				}
			}

			node = node->execute(tdbb, request, &exeState);

			if (exeState.exit) {
				return node;
			}
		}	// try
		catch (const Exception& ex) {
			ex.stuffException(tdbb->tdbb_status_vector);

			request->adjustCallerStats();

			// Skip this handling for errors coming from the nested looper calls,
			// as they're already handled properly. The only need is to undo
			// our own savepoints.
			if (exeState.catchDisabled) {
				// Put cleanup off till the point where it has meaning to avoid
				// sequence 1->2->3->4 being undone as 4->3->2->1 instead of 4->1

				ERR_punt();
			}

			// If the database is already bug-checked, then get out
			if (dbb->dbb_flags & DBB_bugcheck)
				status_exception::raise(tdbb->tdbb_status_vector);

			exeState.errorPending = true;
			exeState.catchDisabled = true;
			request->req_operation = Request::req_unwind;
			request->req_label = 0;

			if (!(tdbb->tdbb_flags & TDBB_stack_trace_done) && !(tdbb->tdbb_flags & TDBB_sys_error))
			{
				stuff_stack_trace(request);
				tdbb->tdbb_flags |= TDBB_stack_trace_done;
			}
		}
	} // while()

	request->adjustCallerStats();


	// If there is no node, assume we have finished processing the
	// request unless we are in the middle of processing an
	// asynchronous message

	if (!node) {
		// Close active cursors
		for (const Cursor* const* ptr = request->req_cursors.begin();
			 ptr < request->req_cursors.end(); ++ptr)
		{
			if (*ptr)
				(*ptr)->close(tdbb);
		}

		if (!exeState.errorPending)
			TRA_release_request_snapshot(tdbb, request);

		request->req_flags &= ~(req_active | req_reserved);
		request->invalidateTimeStamp();
		release_blobs(tdbb, request);
	}

	request->req_next = node;

	request->req_caller = NULL;

	// In the case of a pending error condition (one which did not
	// result in a exception to the top of looper), we need to
	// release the request snapshot

	if (exeState.errorPending) {
		TRA_release_request_snapshot(tdbb, request);
		ERR_punt();
	}

	if (request->req_flags & req_abort)
		ERR_post(Arg::Gds(isc_req_sync));

	return node;
}

Видимо, функцию назвали EXE_looper(), потому что её логика представляет собой цикл, в котором вызывается node->execute(), который возвращает следующий node. Начальный node передаётся как параметр функции. Выйти из цикла метод node::execute() может аж четырьмя способами:

  1. Вернуть null

  2. Поставить exeState.exit

  3. Поставить в request->req_flags флаг req_stall

  4. Выбросить исключение

Чем отличается пункт 1 от пунктов 2 и 3? Тем, что пункты 2 и 3 вернут какой-то другой node, который будет также возвращён из EXE_looper(), то есть это такой способ сказать, что мы не добрались до конца, но сейчас уже не будем дальше ничего выполнять. Завершающая часть функции выполняет очищающие действия, из важного для нас тут только то, что node, который был возвращён из последнего execute(), сохраняется в request->req_next.

Подведём небольшой итог. Функциональность подсистемы Exe состоит в том, что в цикле выполняются некие StmtNode, и они сами решают, кто из них будет выполняться следующим. Откуда берётся самый первый StmtNode? Если зашли из EXE_receive(), то из request->req_next. Вообще Request выглядит как очередной класс-контекст с большим количеством полей, которые заполняются в одном месте, а используются сильно позже, но кто я такой, чтобы учить разработчиков Firebird программировать?

Заключение

Мы попробовали рассмотреть те действия, которые Firebird выполняет, чтобы отдать пользователю одну строку с результатом выполнения запроса. С точки зрения производительности важно отследить две вещи: сколько копирований из памяти в память происходит для данных, и насколько часто выполняются циклы. В нашем случае цикл по возвращаемым столбцам есть только в DsqlDmlRequest::mapInOut(), и там происходит копирование значений параметров из буфера в буфер по одному. Второе копирование происходит в функции EXE_receive(), но там копируется всё сообщение целиком.

Самой глубокой точкой погружения стала функция EXE_looper(), которая выполняет цепочку операций, представленных потомками класса StmtNode. Причём вызывающая функция EXE_receive() строго ожидает, что request->req_message будет содержать StallNode (это класс-потомок StmtNode), выполнение начинается с того StmtNode, который хранится request->req_next. При этом тот StmtNode, который возвращается из EXE_looper(), нигде не используется, он просто выбрасывается! Функция EXE_receive() берёт результат выполнения из request->req_message, в котором ожидается MessageNode (это тоже класс-потомок StmtNode). Каким образом он туда попадёт, мы пока не знаем, но можем смело предположить, что это произойдёт внутри одного из методов execute() у потомков StmtNode, например в MessageNode::execute(), он же выполнялся последним. Проверим это в следующей части статьи.

Самостоятельная работа

  1. Метод EXE_receive() вызывает execute_looper() два раза. С какого StmtNode начнётся выполнение второй раз?

Что не вошло в статью

Как можно было заметить, метод JResultSet::fetch() отдаёт строки по одной, но неужели они и по сети передаются по одной? Конечно же нет. Сетевой запрос op_fetch содержит количество строк, которое он запрашивает. В функции, которая в стектрейсе обозначена как firebird.exe!rem_port::fetch() , извлекается это количество (sqldata->p_sqldata_messages), и в цикле вызывается cursor->fetchNext().

Похоже, недавно был сделан коммит, который слегка отрефакторил тот код, который мы изучали. Будет отличный повод посмотреть, что поменялось и зачем.

Комментарии (0)