Почему GZIP сыпал исключениями только в жару и в дождь
В квартире стояла летняя жара. Впрочем, ненадолго: хозяин квартиры как раз привез мне кондиционер, и его пора было запускать.
Через несколько недель
Я писал на C# программу для распаковки gzip-файлов и неожиданно поймал странное исключение, из которого следовало, что архив поврежден:
Unhandled exception. System.IO.InvalidDataException: The archive entry was compressed using an unsupported compression method.
System.IO.Compression.Inflater.Inflate(FlushCode flushCode)
at System.IO.Compression.Inflater.ReadInflateOutput(Byte* bufPtr, Int32 length, FlushCode flushCode, Int32& bytesRead)
at System.IO.Compression.Inflater.ReadOutput(Byte* bufPtr, Int32 length, Int32& bytesRead)
at System.IO.Compression.Inflater.InflateVerified(Byte* bufPtr, Int32 length)
at System.IO.Compression.DeflateStream.ReadCore(Span`1 buffer)
at System.IO.Compression.DeflateStream.Read(Byte[] array, Int32 offset, Int32 count)
at System.IO.StreamReader.ReadBuffer()
at System.IO.StreamReader.ReadLine()
at MyApp.Program.ReadAllZippedLines(String filename)+MoveNext()
at System.Linq.Enumerable.EnumerablePartition`1.MoveNext()
at MyApp.Program.Main(String[] args)
at MyApp.Program.<Main>(String[] args)Ситуация выглядела дико: ошибка намекала на поврежденный архив, хотя сам файл не выглядел реально битым. Сказать, что это напрягало, значит ничего не сказать: если бы данные и правда оказались повреждены, потери были бы серьезные. Я просто запустил программу еще раз, и она... отработала нормально. Странно. Первая мысль была такой: может, Windows в этот момент держала файл открытым из-за антивирусной проверки, а я просто попал в неудачный момент.
Но через несколько минут ошибка вернулась. Я выключил Windows Defender, перезагрузил ноутбук и попробовал снова. Без толку. Самое неприятное было в том, что ошибка не воспроизводилась стабильно: иногда все работало, иногда нет.
На очередном запуске Visual Studio остановилась на нужной строке благодаря обработчику исключений. Я смог посмотреть, что происходит внутри C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.16\System.IO.Compression.dll:
internal int ReadCore(Span<byte> buffer) {
this.EnsureDecompressionMode();
this.EnsureNotDisposed();
this.EnsureBufferInitialized();
int start = 0;
while (true) {
do {
int num = this._inflater.Inflate(buffer.Slice(start));
start += num;
if (start == buffer.Length || this._inflater.Finished() && (!this._inflater.IsGzipStream() || !this._inflater.NeedsInput()))
goto label_7;
}
while (!this._inflater.NeedsInput());
int count = this._stream.Read(this._buffer, 0, this._buffer.Length);
if (count > 0) {
if (count <= this._buffer.Length) // здесь условие оказалось false
this._inflater.SetInput(this._buffer, 0, count);
else
break;
} else
goto label_7;
}
throw new InvalidDataException(SR.GenericInvalidData);
label_7:
return start;
}Почему вообще count иногда оказывался больше длины буфера? Выглядело это так, будто либо размер буфера, либо сама переменная count в какой-то момент получали некорректное значение. Сам файл при этом не менялся, по крайней мере если верить SHA-1. Правда, во второй раз я даже не смог посчитать хеш: расширение для Проводника Windows, которым я пользовался, само упало с какой-то внутренней ошибкой.
Неужели я наткнулся на состояние гонки? Впрочем, сам распаковщик gzip не выглядел многопоточным, но я все равно пошел глубже по стеку вызовов:
private ZLibNative.ErrorCode Inflate(ZLibNative.FlushCode flushCode) {
ZLibNative.ErrorCode errorCode;
try {
errorCode = this._zlibStream.Inflate(flushCode);
} catch (Exception ex) {
throw new ZLibException(SR.ZLibErrorDLLLoadError, ex);
}
switch (errorCode) {
case ZLibNative.ErrorCode.BufError:
return errorCode;
case ZLibNative.ErrorCode.MemError:
throw new ZLibException(SR.ZLibErrorNotEnoughMemory, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage());
case ZLibNative.ErrorCode.DataError:
throw new InvalidDataException(SR.UnsupportedCompression);
case ZLibNative.ErrorCode.StreamError:
throw new ZLibException(SR.ZLibErrorInconsistentStream, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage());
case ZLibNative.ErrorCode.Ok:
case ZLibNative.ErrorCode.StreamEnd:
return errorCode;
default:
throw new ZLibException(SR.ZLibErrorUnexpected, "inflate_", (int) errorCode, this._zlibStream.GetErrorMessage());
}
}Иногда возвращался DataError, иногда StreamError. То есть что-то было не так либо с данными, либо с потоком. Формулировка, мягко говоря, не самая информативная.
Чтобы исключить банальную ошибку в коде, я попробовал распаковать gzip-файл из bash через gzip -dc file. В ответ прилетела странная ошибка “can’t seek file descriptor” при попытке чтения файла. Генерируется она вот здесь, в исходниках bash. Заодно я прогнал еще несколько gzip-файлов, и они тоже не распаковывались.
К этому моменту я уже всерьез подозревал порчу данных. Быстро открыл CrystalDiskInfo, проверил, нет ли переназначенных секторов, но ничего такого не увидел. Потом запустил sfc.exe /SCANNOW, и, насколько помню, он действительно нашел ошибки и исправил их. Я прогонял его еще и еще, и каждый раз он что-то чинил. Где-то на пятый запуск, кажется, ошибок больше не осталось.
В следующие несколько недель с ноутбуком начали происходить совсем уж странные вещи. Приложения запускались бесконечно долго, а иногда не запускались вовсе, исчезали иконки, текст превращался в кашу. Я уже решил, что ноутбук просто умирает. Но оставалась одна загадка: на батарее ничего подобного не происходило. Все странности начинались только при питании от сети. И почему тогда проблемы появились еще и у монитора, который я совсем недавно купил?
Пора копать глубже
Особенно меня насторожило то, что держать руки на корпусе стало неприятно. Это был уже немолодой MacBook Pro 2012 года с Boot Camp, но на алюминии не было никаких видимых следов износа. И снова всплыл тот же паттерн: дискомфорт появлялся только при работе от сети.
Отступление: к этому моменту мне уже два или три раза приходилось восстанавливать Visual Studio через Repair из-за каких-то совсем диких ошибок, по которым Google ничего не находил.
Полная картина сложилась, когда меня как следует ударило током от корпуса ноутбука. Тут я впервые всерьез подумал: а вдруг проблема вообще в электрике?
Ток там, где его быть не должно
К этому моменту у меня накопился такой набор симптомов:
На новом мониторе появлялись странные зелено-красные помехи.
На старом мониторе появлялись такие же зелено-красные помехи.
Трекпад неприятно пощипывал током.
Похоже, местами портились данные.
От ноутбука ощутимо било током.
Иногда свет в квартире после выключения как будто сам по себе чуть-чуть загорался снова.
Слишком уж странно выглядело совпадение, что и старый, и новый монитор показывали один и тот же набор артефактов. В начале года я еще и менял батарею в ноутбуке, так что не исключал и собственную ошибку.
Что вообще может заставить проводить ток то, что по идее проводить его не должно? Вариантов хватало:
Какое-то устройство подключено или собрано неправильно.
Проблемы с заземлением.
Что-то еще, о чем я пока не подумал.
Чтобы исключить влияние других устройств, я выдернул все из сетевого фильтра и включил ноутбук напрямую в розетку в ванной (она подключена через УЗО). После этого нагрузил его Prime95 примерно на 15 минут. Все выглядело нормально. Потом я подключил остальное обратно, и поначалу тоже было тихо.
А затем проблемы вернулись. Но не всегда, как мне тогда казалось. Со временем я заметил корреляцию: они проявлялись, когда на улице шел дождь или просто была высокая влажность. Неужели из-за сырости где-то что-то коротит?
Apple пишет, что использовать MacBook стоит при относительной влажности от 0% до 95% без конденсации. По температуре воздуха я тоже оставался в допустимых пределах. На всякий случай я выкрутил кондиционер на максимум, чтобы сильнее снизить и температуру, и влажность. Но стало только хуже.
Тогда я подумал: может, воздух, наоборот, слишком сухой? Попробовал поднять температуру на кондиционере, но это тоже ничего не дало.
Зато я заметил другое: проблемы коррелировали с включенным кондиционером, а кондиционер, в свою очередь, включался именно тогда, когда становилось жарко и душно.
К этому моменту я уже почти не сомневался, что дело в электрике. Я попросил хозяина квартиры прислать мастера. Пришел техник и посоветовал купить ИБП с AVR, то есть с автоматической стабилизацией напряжения.
Это такой ИБП, который сам подравнивает напряжение до нужного уровня, если оно проседает ниже порога. В моем случае речь шла о 120 В. Я и сам замечал, что свет иногда тускнеет, поэтому техник предположил, что все мои проблемы и с железом, и с софтом могут быть просто следствием просадок по питанию.
Я купил такой ИБП и сперва был очень доволен: он постоянно пищал, а значит, действительно фиксировал какие-то проблемы и компенсировал просадки напряжения.
Слишком уж постоянно. Пищал на созвонах, пищал за обедом, пищал, когда я пытался уснуть. Стоишь, пьешь воду, и тут... пиииип! Хорошо, что в тот момент я просто пил воду.
В какой-то момент я отключил звук, потому что он начал всерьез мешать и будить меня по ночам. По статистике ИБП за несколько дней уже успел отработать как минимум 20 инцидентов с питанием. "Ничего себе, пашет не зря", подумал я.
К сожалению, еще через несколько дней все проблемы вернулись.
Заземление
Следующим логичным шагом было проверить заземление. Заземление - это путь, по которому лишняя или нежелательная электрическая энергия безопасно уходит в грунт. Если с этим есть проблема, то лишний ток может не уходить куда должен. Терминологию я, возможно, местами коверкаю: я не электрик. Но смысл был именно такой.
Простой тестер розеток за 10 долларов быстро подтвердил подозрение: заземления не было.
Отступление: к этому моменту ноутбук стал почти непригоден к использованию даже на батарее. Начались случайные выключения, экран мог просто почернеть. В журнале событий сыпались записи, связанные с проблемами питания APIC. В итоге я параллельно купил новый ноутбук.
После этого я снова попросил хозяина квартиры прислать уже именно электрика. Он задал много вопросов, а потом предложил подключить кондиционер в другую розетку, потому что в квартире было две отдельные линии питания. Я так прожил несколько дней, но симптомы снова вернулись.
Я попросил приехать еще раз. На этот раз мне поставили совершенно новую розетку и заземлили ее (как мне сказали) через подключение водонагревателя: по крайней мере, после этого тестер начал показывать корректное заземление.
После этого, пусть один ноутбук к тому моменту уже и был фактически списан, проблемы исчезли. Никаких странных цветов на мониторе, никакого трекпада, от которого щиплет током, никаких ударов током от корпуса.
К чему я все это: иногда отладка выходит далеко за пределы IDE и сталкивается с реальным миром.