В общем - "в третий раз закинул старик невод"...
Точнее в третий раз не удался выпуск внутреннего продукта.
По моей вине.
Хотя и "со всеми пирогами", тестами, фабриками и прочим и прочим.
Тесты всякие разные - проходят.
Реальный софт в боевых условиях - не работает.
То READ_ERROR, то WRITE_ERROR. При распределённом доступе.
В общем - пока я с этим не разберусь и не переосмыслю, и не напишу - "рецепты лечения" - в блог писать - не буду.
Ибо - незачем.
Ибо - "теория слаба без практики".
Незачем писать, про "коней в вакууме", если у "тебя самого эти кони не работают".
Пока - сваяли "нагрузочный тест".
Оставил его "молотить" на ночь.
Завтра с утра - погляжу.
Может быть будем писать "другой нагрузочный тест".
Давненько таких epic fail'ов не случалось.
Одно только могу сказать "не про себя" - hResult и прочие "ErrorCode" скажем так - "не очень хорошая придумка". Ну как использовать конечно... Я не всегда правильно использовал...
Было:
SetFilePos(hFile, aPos);
FileWrite(hFile, @SomeValue, SizeOf(SomeValue));
SetFilePos(hFile, aPos);
FileRead(hFile, @SomeOtherValue, SizeOf(SomeOtherValue));
Assert(SomeValue = SomeOtherValue);
Стало:
SetFilePos(hFile, aPos);
FileWrite(hFile, @SomeValue, SizeOf(SomeValue));
SetFilePos(hFile, aPos);
SysCheck(FileRead(hFile, @SomeOtherValue, SizeOf(SomeOtherValue)));
// - тут стало "иногда" падать, хотя и SomeOtherValue = SomeValue
Assert(SomeValue = SomeOtherValue);
- что удивительно - без SysCheck - проверка проходила.
Т.е. ошибку таки возвращали, но ПРОВЕРКА проходила.
Т.е. вот так работает:
SetFilePos(hFile, aPos);
FileWrite(hFile, @SomeValue, SizeOf(SomeValue));
SetFilePos(hFile, aPos);
try
SysCheck(FileRead(hFile, @SomeOtherValue, SizeOf(SomeOtherValue)));
// - тут стало "иногда" падать, хотя и SomeOtherValue = SomeValue
finally
Assert(SomeValue = SomeOtherValue);
// - тут НЕ ПАДАЕМ, хотя и "падаем" выше, на SysCheck
end;
Т.е. "иногда читается то, что нужно", но "с ошибкой.
И БЕЗ проверки на ошибки - всё работало, но "подглючивало", с "проверкой на ошибки" - стало "чаще падать".
В условиях распределённой гетерогенной среды.
С "дохлыми" и "полудохлыми" компьютерами.
И разными версиями Windows. Вплоть до "доисторических".
Почему? Пока - непонятно.
Грешу всё же на "
собственные кривые руки", а не на "парней из Microsoft".
Ибо проще всего - "свалить на других парней".
Проще, но
неконструктивно.
И кстати если написать:
SetFilePos(hFile, aPos);
FileWrite(hFile, @SomeValue, SizeOf(SomeValue));
l_TryCount := 0;
while (l_TryCount < 100) do
begin
Inc(l_TryCount);
SetFilePos(hFile, aPos);
try
SysCheck(FileRead(hFile, @SomeOtherValue, SizeOf(SomeOtherValue)));
// - тут стало "иногда" падать, хотя и SomeOtherValue = SomeValue
except
if (l_TryCount < 100) then
continue
else
raise;
end;//try..except
break;
end;
Assert(SomeValue = SomeOtherValue);
То опять же - "падает гораздо реже".
Повод "для раздумий".
К сожалению - на "синтетических тестах" это - не повторяется.
Ну и "до кучи" - там ещё участвуют LockRegion/UnlockRegion.
Естественно - "правильно расставленные" и "правильным образом" обрамлённые SysCheck etc.
НО - похоже -
всё дело всё же в их присутствии.
Без них - "типа работает", но с "конкурентным доступом" - плохо. Что и понятно.
Будем переходить на новый уровень тестирования.
P.S. весь приведённый код выше - это конечно же - "псевдокод". Лишь для иллюстрации проблем. "Запятые" там скорее всего - стоят неправильно. Ну и SysCheck при FileWrite -
преднамеренно - опущен. Поверьте - он там есть.
И кроме того там ещё WrittenSize и ReadSize - проверяются.
Но эти детали - тоже
специально - опущены.
P.P.S. Код
без SysCheck и OleCheck уже лет 15-ть как
работает, но "подглючивает" (время от времени, именно время от времени - получаются некорректные данные, "неприятно", но "жить типа можно"), собственно почему я и "полез разбираться" и писать SysCheck и OleCheck.
И "огрёб по полной программе".
Чего именно "огрёб"? Пока - не понял.
Ещё раз повторю - "далеко не на всех клиентских станциях" это повторяется. Возможно - "руки кривые".
В общем - "не забывайте про коды ошибок", но их "обработка" - тоже "не всегда однозначна".
Когда уличу себя в "кривых руках" - тогда обязательно напишу.
P.P.P.S. Ну и ещё я "вкрутил логирование" проблемных операций. И.. И.. Получил "кошку Шрёдингера". Логирование стало влиять на "бизнес-логику". Хотя бы "в части временн'ых задержек". Ну, что и понятно.
Тоже - "отдельная тема".
P.P.P.P.S. Да. И код выше приведён для
одного клиента. А не то, что
один пишет, а
другой читает. Для
разных клиентов - я всё понимаю. А
для одного - пока не понимаю. Но скоро - надеюсь пойму.
P.P.P.P.S. И ещё.
Версию вида:
function DoRead(...): LongBool;
begin
FileRead(hFile ...);
// - тут "забыли" вернуть результат
end;
...
SysCheck(DoRead(...));
// - тут проверяем "мусор"
-- я уже проверил.
"Навскидку" - нету "неинициализированных переменных".
P.P.P.P.P.S. Тесты кстати пока "ещё молотят"... Без ошибок :-( Что "навевает грусть". Буду далее - утром смотреть.
P.P.P.P.P.S. Как выяснилось - чтобы были проблемы надо две вещи:
1. Доступ по UNC-путям, то есть по путям вида - \\server\resource\path\filename.
2. Обязательное использование LockFile.
P.P.P.P.P.P.S. Вчера тесты отработали
без ошибок. Намолотили порядка 3 Гб данных.
Сегодня запустил тесты с двух машин. Завтра буду смотреть на результаты.
Потом буду запускать с 3-х, 4-х, 5-ти и т.д. и т.п.
P.P.P.P.P.P.P.S. И ещё сегодня я нашёл два "бутылочных горлышка" - AllocNewFATAlement и AllocNewCluster. Они в свою очередь ведут к LockFile, Лочим заголовок. Где записана информация о структуре хранилища. И все пользователи при записи "бъются от эти залочки".
И знаю уже как это разрулить.
Надо упреждающе аллоцировать не один FATElement и Cluster, а сразу несколько (пять, десять, двадцать). Одним куском. Учитывая, что кластеров в файле обычно не один и не два и даже не десять, то это - эффективно. И держать список свободных локально у клиента. А потом когда клиент закрывается - возвращать их в список свободных. Т.е. "не пригодившихся". Чтобы из потом могли использовать другие клиенты.
Есть конечно вероятность - потерять элементы, если клиент завершится аварийно.
Но зато - мы реже попадаем в "бутылочное горлышко".
Потому, что мы можем написать.
if AllocatedFatElements.Empty then
begin
// - тут есть межПРОЦЕССНОЕ "бутылочное горлышко"
Lock;
try
Result := AllocNewFatElement;
for l_Index := 0 to 10 do
AllocatedFatElements.Add(AllocNewFatElement);
finally
Unlock;
end//try..finally
end
else
// - тут ТОЛЬКО межПОТОЧНОЕ "бутылочное горлышко" (потому что AllocatedFatElements - естественно - многопоточно-защищён)
Result := AllocatedFatElements.GetLastAndDeleteIt;
Вместо:
Lock;
// - а тут - ВСЕГДА - многоПРОЦЕССОРНОЕ "бутылочное горлышко"
try
Result := AllocNewFatElement;
finally
Unlock;
end;//try..finally
Ну и аналогично для кластеров.
А даже если и элементы "провиснут", то сильно хуже не будет, хранилище - не разрушится. Просто в нём будут "дырки".
Но учитывая тот факт, что у нас ночью происходит "ночной Update", если он возможен конечно, то хранилище - всё равно - перепаковывается. Т.е. "дырки" к "утру" - всё равно исчезнут.
И будет перепакованная постоянная часть без дырок. И "пустая" переменная часть.
Куда клиенты в течении дня пишут свои версии документов.
А следующей ночью - процесс опять повторяется.
Если конечно к базе не подключены работающие пользователи.
Отдельный пост -
тут.
Оговорюсь - "это всё про внутренние продукты". Про внешние продукты - рассказывать не буду.
Ну и "для тех кто дочитал", вот так вот выглядит задача:
"Дефицит" :-(