Фольклор програмістів та інженерів (частина 1)

Фольклор програмістів та інженерів (частина 1)

Це добірка історій з інтернету про те, як у багів іноді бувають неймовірні прояви. Можливо вам теж є що розповісти.

Алергія автомобіля на ванільне морозиво

Історія для інженерів, які розуміють, що очевидне не завжди є рішенням, і що хоч би як факти здавалися неправдоподібними, це все одно факти. До підрозділу Pontiac Division корпорації General Motors надійшла скарга:

Пишу вам вдруге, і не звинувачую вас у тому, що ви не відповідаєте, адже це звучить шалено. Наша родина має традицію: щовечора після вечері є морозиво. Сорти морозива щоразу змінюються, і повечерявши, вся сім'я вибирає, яке морозиво потрібно купити, після чого я їду до магазину. Нещодавно я купив новий Pontiac, і відтоді мої поїздки за морозивом перетворилися на проблему. Чи бачите, щоразу, коли я купую ванільне морозиво і повертаюся з магазину, машина не заводиться. Якщо я приношу будь-яке інше морозиво, машина заводиться без проблем. Хочу поставити серйозне питання, незалежно від того, наскільки безглуздо це прозвучить: «Що такого є в Pontiac, через що воно не заводиться, коли я приношу ванільне морозиво, але при цьому легко заводиться, якщо я приношу морозиво з іншим смаком? ».

Як ви розумієте, президент підрозділу поставився до листа скептично. Однак про всяк випадок відправив інженера перевірити. Той був здивований, що його зустрів забезпечений, добре освічений чоловік, який живе у чудовому районі. Вони домовилися зустрітися одразу після вечері, щоб удвох поїхати до магазину за морозивом. Того вечора було ванільне, і коли вони повернулися до машини, та не завелася.

Інженер приїжджав ще три вечори. Вперше морозиво було шоколадним. Машина завелася. Вдруге було полуничне морозиво. Машина завелася. На третій вечір він попросив узяти ванільне. Машина не завелася.

Здорово розсудивши, інженер відмовився вірити в алергію автомобіля на ванільне морозиво. Тому домовився із власником машини, що він продовжить свої візити, доки не знайде вирішення проблеми. І попутно почав робити нотатки: записував всю інформацію, час доби, сорт бензину, час приїзду та повернення з магазину тощо.

Незабаром інженер здогадався: власник машини витрачав на покупку ванільного морозива менше часу. Причина була у розкладанні товару у магазині. Ванільне морозиво було найпопулярнішим і лежало в окремому морозильнику в передній частині магазину, щоб його було легко знайти. А всі інші сорти знаходилися в задній частині магазину, і на пошук потрібного сорту та оплату йшло значно більше часу.

Тепер питання було до інженера: чому машина не заводилася, якщо з того моменту, коли заглушили двигун, минало менше часу? Оскільки проблемою став час, а не ванільне морозиво, інженер швидко знайшов відповідь: справа була в газовій пробці. Вона виникала щовечора, але коли власник машини витрачав на пошук морозива більше часу, двигун встигав досить охолонути і спокійно заводився. А коли чоловік купував ванільне морозиво, двигун ще залишався надто гарячим і газова пробка не встигала розсмоктатися.

Мораль: навіть шалені проблеми іноді бувають реальними.

Crash Bandicoot

Переживати таке болісно. Як програміст, ти звикаєш звинувачувати свій код у першу чергу, в другу, в третю… і десь у десятитисячну чергу звинувачуєш компілятор. І ще нижче за списком уже звинувачуєш обладнання.

Ось моя історія про баг заліза.

Для гри Crash Bandicoot я написав код для завантаження та збереження на картку пам'яті. Для такого самовдоволеного розробника ігор це було ніби прогулятися парком: я вважав, що робота займе кілька днів. Однак у результаті налагоджував код шість тижнів. Принагідно я вирішував і інші завдання, але кожні кілька днів на кілька годин повертався до цього коду. То була агонія.

Симптом виглядав так: коли зберігаєш поточне проходження гри і звертаєшся до карти пам'яті, майже завжди все проходить нормально. Короткий запис часто пошкоджує картку пам'яті. Коли гравець намагається зберегтися, він не тільки не зберігається, але ще й руйнує картку. Млинець.

Через деякий час наш продюсер у Sony, Конні Бус, почала панікувати. Ми не могли відвантажити гру з таким багом, і через шість тижнів я не розумів, у чому причина цієї проблеми. Через Конні ми зв'язалися з іншими розробниками PS1: хто стикався з подібним? Ні. Ні в кого не було проблем із карткою пам'яті.

Коли в тебе немає ідей щодо налагодження, то практично єдиним підходом залишається «розділяти і панувати»: прибираєш все більше і більше коду з помилкової програми, поки не залишається порівняно невеликий фрагмент, при роботі якого досі виникає проблема. Тобто відрізаєш від програми по шматку, доки не залишається та частина, що містить баг.

Але річ у тому, що дуже важко вирізати шматки з відеоігри. Як її запускати, якщо ти прибрав код, що емулює гравітацію? Чи малює персонажів?

Тому доводиться замінювати цілі модулі заглушками, які вдають, що роблять щось корисне, а насправді виконують щось дуже просте, що не може містити помилок. Доводиться писати такі милиці, щоб гра хоч би працювала. Це повільний та болісний процес.

Коротше, я це зробив. Видаляв нові і нові шматки коду, поки не залишився початковий код, який налаштовує систему для запуску гри, ініціалізує обладнання для малювання і т.д. Звичайно, на цій стадії я не міг зробити меню збереження та завантаження, тому що довелося б робити заглушку для всього коду, що відповідає за графік. Але я міг прикинутись користувачем, який використовує (невидимий) екран збереження та завантаження і просить зберегти, а потім записати на картку пам'яті.

В результаті у мене залишився невеликий фрагмент коду, з яким все ще виникала вищезгадана проблема - але досі це відбувалося випадково! Найчастіше все працювало нормально, але зрідка виникали збої. Я прибрав майже весь код гри, але баг все ще жив. Це спантеличило: код, що залишився, насправді нічого не робив.

Якоїсь миті, мабуть, о третій ранку, мені на думку прийшла думка. Операції читання та запису (введення-виведення) мають на увазі точний час виконання. Коли працюєш із жорстким диском, картою пам'яті або Bluetooth-модулем, низькорівневий код, який відповідає за читання та запис, робить це відповідно до тактових імпульсів.

За допомогою годинника пристрій, який не пов'язаний безпосередньо з процесором, синхронізується з виконуваним в процесорі кодом. Годинник визначає частоту бодів - швидкість передачі даних. Якщо з таймінгами виникає плутанина, то плутається або обладнання, або ПЗ, або вони обидва. І це дуже погано, тому що дані можуть зашкодити.

А раптом щось у нашому коді плутає таймінги? Я перевірив усе, що з цим пов'язано, в коді тестової програми, і помітив, що ми задали програмованого таймера в PS1 частоту 1 кГц (1000 тактів в секунду). Це досить багато, за умовчанням при запуску приставки він працює із частотою 100 Гц. І більшість ігор використовує саме цю частоту.

Енді, розробник гри, задав таймеру частоту 1 кГц щоб рухи обчислювалися точніше. Енді схильний до надмірності, і якщо ми емулюємо гравітацію, то робимо це настільки точно, наскільки це можливо!

Але що якщо прискорення таймера якось вплинуло на загальний таймінг програми, а значить і на годинник, який регулює частоту бодів для карти пам'яті?

Я закоментував код із таймером. Помилка не повторювалася. Але це не означає, що ми її виправили, адже збій виникав випадково. А раптом мені просто пощастило?

За кілька днів я знову експериментував із тестовою програмою. Баг не повторювався. Я повернувся до повної кодової бази гри та змінив код збереження та завантаження так, щоб програмований таймер скидався у вихідне значення (100 Гц) перед зверненням до картки пам'яті, а потім знову повертався до 1 кГц. Збоїв більше не виникало.

Але чому так сталося?

Я знову повернувся до тестової програми. Спробував виявити якусь закономірність у виникненні помилки при таймері в 1 кГц. Зрештою, я помітив, що помилка виникає, коли хтось грає з контролером PS1. Оскільки сам я рідко це робив - навіщо мені контролер при тестуванні коду збереження та завантаження? — то я не помічав цієї залежності. Але одного разу один із наших художників чекав, коли я закінчу тестування, — напевно, на той момент я лаявся, — і нервово крутив контролер у руках. Виникла помилка. «Стривай, що?! Ану зроби так знову!».

Коли я зрозумів, що ці дві події взаємопов'язані, я зміг легко відтворити помилку: почав записувати на карту пам'яті, поворухнув контролер, зіпсував карту пам'яті. Для мене це виглядало як апаратний баг.

Я прийшов до Конні і розповів про своє відкриття. Вона передала інформацію одному з інженерів, котрий проектував PS1. "Неможливо, - відповів він, - це не може бути апаратною проблемою". Я попросив Конні зробити нам розмову.

Інженер подзвонив мені, і ми посперечалися з ним його ламаною англійською і моєю (вкрай) ламаною японською. Нарешті я сказав: «Давайте просто надішлю свою тестову програму в 30 рядків, коли рух контролера призводить до багу». Він погодився. Заявив, що це втрата часу, і що він дуже зайнятий роботою над новим проектом, але поступиться, тому що ми дуже важливий розробник для Sony. Я підчистив свою тестову програму та відправив йому.

Наступного вечора (ми були в Лос-Анджелесі, а він у Токіо) він подзвонив мені і зніяковіло вибачився. То була апаратна проблема.

Я не знаю, в чому саме полягав баг, але, наскільки я чув у штаб-квартирі Sony, якщо задати таймеру досить високе значення, це заважало компонентам на материнській платі поблизу кристала таймера. Одним із них був контролер частоти бодів для карти пам'яті, який також задавав частоту бодів для контролерів. Я не інженер, тож міг щось наплутати.

Але суть у тому, що між компонентами материнської плати виникали перешкоди. І при одночасної передачі даних через порт контролера і порт карти пам'яті при таймері, що працює з частотою 1 кГц, біти пропадали, дані губилися, а карта ушкоджувалася.

Збійні корови

У 1980-х мій ментор Сергій писав ПЗ для СМ-1800, радянського клону PDP-11. Цей мікрокомп'ютер щойно встановили на залізничній станції під Свердловськом, важливим транспортним вузлом СРСР. Нова система була спроектована для маршрутизації вагонів та вантажопотоків. Але в ній виявився прикру баг, який приводив до випадкових збоїв і падінь. Падіння виникали завжди, коли хтось ішов увечері додому. Але незважаючи на ретельне розслідування наступного дня, при всіх ручних та автоматичних тестах комп'ютер працював коректно. Зазвичай це свідчить про стан гонки або якийсь інший баг конкурентності, який проявляється за певних умов. Втомившись від дзвінків пізньої ночі, Сергій вирішив докопатися до суті, і насамперед зрозуміти, які умови на сортувальній станції призводили до поломки комп'ютера.

Спочатку він зібрав статистику всіх незрозумілих падінь і побудував графік за датами та часом. Паттерн був очевидним. Поспостерігавши ще кілька днів, Сергій зрозумів, що може спрогнозувати час майбутніх системних збоїв.

Незабаром він з'ясував, що збої виникали тільки тоді, коли на станції сортували вагони з великою рогатою худобою з північної України та західної Росії, які прямували на скотобійню. Це само по собі було дивно, адже скотобійню постачали господарства, які знаходилися набагато ближче, у Казахстані.

Чорнобильська АЕС вибухнула 1986-го, і радіоактивні опади зробили непридатними до проживання прилеглі території. Забруднення зазнали великі території в північній Україні, Білорусії та західній Росії. Запідозривши високий рівень радіації у вагонах, Сергій розробив метод перевірки цієї теорії. Населення мати дозиметри заборонялося, тому Сергій проставився кільком військовим на залізничній станції. Після кількох порцій горілки йому вдалося переконати солдата виміряти рівень радіації в одному із підозрілих вагонів. Виявилося, що рівень у рази перевищує нормальні значення.

Мало того, що худоба сильно фонувала радіацією, її рівень був настільки великий, що це призводило до випадкового випадання бітів у пам'яті СМ-1800, що стояла в будівлі поряд зі станцією.

У СРСР виникав брак продуктів харчування, і влада вирішила змішувати «чорнобильське» м'ясо з м'ясом з інших областей країни. Це дозволяло зменшити загальний рівень радіоактивності без втрати цінних ресурсів. Дізнавшись про це, Сергій одразу заповнив документи на еміграцію. А падіння комп'ютера припинилися самі по собі, коли рівень радіації знизився з часом.

Трубами

Колись Movietech Solutions створила ПЗ для кінотеатрів, призначене для обліку та продажу квитків та загального управління. DOS-версія флагманської програми була досить популярна серед невеликих та середніх мереж кінотеатрів у Північній Америці. Тому не дивно, що коли анонсували версію під Windows 95, інтегровану з новітніми сенсорними екранами та кіосками самообслуговування, а також оснащену всілякими засобами звітності, вона також швидко стала популярною. Найчастіше оновлення проходило без проблем. ІТ-фахівці на місцях встановлювали нове обладнання, мігрували дані, і бізнес продовжувався. За винятком випадків, коли не продовжувався. Коли таке відбувалося, компанія відправляла Джеймса на прізвисько «Чистильник».

Хоча це прізвисько натякає на мерзенного типу, проте чистильник - це лише поєднання інструктора, установника та майстра на всі руки. Джеймс міг провести кілька днів у клієнта, збираючи всі компоненти воєдино, а потім ще пару днів вчив персонал користуватися новою системою, вирішуючи будь-які проблеми з обладнанням і фактично допомагаючи програмному забезпеченню пройти період становлення.

Тому не дивно, що в цей метушні час Джеймс прийшов вранці в офіс, і не встиг дійти до свого столу, як його зустрів керівник, наповнений кофеїном понад звичайний.

— Боюся, тобі треба якнайшвидше вирушити до Аннаполіса в Новій Шотландії. У них лягла вся система, і після ночі спільної роботи з їхніми інженерами ми не можемо зрозуміти, що сталося. Схоже, що на сервері відмовила мережа. Але лише після того, як система пропрацювала кілька хвилин.

— Вони не повернулися до старої системи? — зовсім серйозно відповів Джеймс, хоча подумки він витріщив очі з подиву.

— Саме: у їхнього айтішника «змінилися пріоритети» і він вирішив піти з їхнім старим сервером. Джеймс, вони встановили систему на шести майданчиках і щойно заплатили за преміальну підтримку, а їхній бізнес зараз ведеться як у 1950-х.

Джеймс трохи випростався.

- Це інша справа. Гаразд, приступаю.

Коли він прибув до Аннаполіса, то насамперед знайшов перший кінотеатр клієнта, в якому виникла проблема. На взятій в аеропорту карті все виглядало пристойно, але околиці потрібної адреси виглядали підозріло. Чи не гетто, але нагадували фільми в жанрі «нуар». Коли Джеймс припаркувався біля узбіччя в центрі, до нього наблизилася повія. Враховуючи розмір Аннаполіса, вона, швидше за все, була єдиною на все місто. Її поява відразу ж нагадала про знаменитого персонажа, який на великому екрані пропонував секс за гроші. Ні, не про Джулію Робертс, а про Джона Войте [натяк на фільм «Опівнічний ковбой» — прим. пров.].

Відправивши повію додому, Джеймс вирушив до кінотеатру. Околиці стали кращими, але все одно створювалося враження мізерності. Не те щоб Джеймс надто переймався. Він уже бував у убогих місцях. А це була Канада, в якій навіть грабіжники досить ввічливі, щоб сказати «дякую» після того, як відібрали ваш гаманець.

Бічний вхід у кінотеатр знаходився у вогкій алеї. Джеймс підійшов до дверей і постукав. Незабаром вона заскрипіла і відкрилася.

- Ви чистильник? - пролунав зсередини хрипкий голос.

— Так, це я… я приїхав, щоби все виправити.

Джеймс пройшов у фойє кінотеатру. Певно, не маючи іншого виходу, персонал почав видавати відвідувачам паперові квитки. Це ускладнювало фінансову звітність, не кажучи вже про більш цікаві подробиці. Але співробітники зустріли Джеймса з полегшенням та негайно відвели до серверної.

На перший погляд, все було в порядку. Джеймс заклався на сервер і перевірив звичайні підозрілі місця. Ніяких проблем. Однак із обережності Джеймс вимкнув сервер, замінив мережеву картку та відкотив систему. Вона моментально запрацювала у повному обсязі. Персонал знову почав продавати квитки.

Джеймс зателефонував Марку і повідомив про ситуацію. Неважко припустити, що Джеймс може захотіти затриматися тут і подивитися, чи не станеться щось несподіване. Він спустився сходами і почав розпитувати співробітників і події. Очевидно, що система перестала працювати. Вони її вимкнули та ввімкнули, все запрацювало. Але за 10 хвилин система відвалилася.

Саме в цей момент сталося щось подібне. Несподівано система для продажу квитків почала видавати помилки. Співробітники зітхнули і згрібли паперові квитки, а Джеймс поспішив до серверної. Із сервером все виглядало добре.

Потім увійшов один із співробітників.

— Система знову працює.

Джеймс був спантеличений, адже він нічого не зробив. Точніше нічого такого, що змусило б систему працювати. Він розлогінився, взяв телефон та зателефонував до служби підтримки своєї компанії. Незабаром у серверну увійшов той самий співробітник.

- Система лежить.

Джеймс глянув на сервер. На екрані танцював цікавий і знайомий візерунок з різнокольорових форм — труби, що хаотично звиваються і переплітаються. Усі ми колись бачили цей скрінсейвер. Він був чудово відмальований і буквально гіпнотизував.


Джеймс натиснув кнопку і візерунок зник. Він поквапився до квиткової каси і по дорозі зустрів співробітника, що повертався до нього.

— Система знову працює.

Якщо можна подумки зробити фейспалм, то саме це Джеймс і зробив. Скрінсейвер. Він використовує OpenGL. І тому під час роботи споживає усі ресурси серверного процесора. В результаті кожне звернення до сервера завершується по таймууту.

Джеймс повернувся в серверну, залогінився і замінив скрінсейвер із чудовими трубами на порожній екран. Тобто замість скрінсейвера, що поглинає 100% ресурсів процесора, поставив інший, що не споживає ресурси. Потім почекав 10 хвилин, щоб перевірити свій здогад.

Коли Джеймс приїхав до наступного кінотеатру, він задумався про те, як пояснити своєму керівнику, що він щойно пролетів 800 км, щоб вимкнути скрінсейвер.

Збій у певну фазу Місяця

Правдива історія. Якось виник програмний баг, який залежав від фази Місяця. Там була невелика підпрограма, яка зазвичай використовувалася в різних програмах MIT для обчислення наближення до справжньої фази Місяця. GLS вбудувала цю підпрограму в програму на LISP, яка під час запису файлу виводила рядок з тимчасовою міткою довжиною майже 80 символів. Дуже рідко перший рядок повідомлення виходив занадто довгим і переходив на наступний рядок. І коли програма потім читала цей файл, вона лаялася. Довжина першого рядка залежала від точної дати та часу, а також від довжини специфікації фази в момент друку тимчасової мітки. Тобто баг у буквальному значенні залежав від фази Місяця!

Перше паперове видання Jargon File (Steele-1983) містило зразок такого рядка, що призводив до описаного бага, проте набірщик «виправив» його. З того часу це описують як «баг фази Місяця».

Однак будьте обережні з припущеннями. Декілька років тому інженери з CERN (European Center for Nuclear Research) зіткнулися з помилками в експериментах, що проводилися на Великому електрон-позитронному колайдері. Оскільки комп'ютери активно обробляють гігантську кількість даних, що генеруються цим пристроєм, перш ніж показати результат вченим, багато хто припускав, що ПЗ якимось чином чутливе до фази Місяця. Декілька відчайдушних інженерів докопалися до істини. Помилка виникала через невелику зміну геометрії кільця завдовжки 27 км через деформацію Землі при проході Місяця! Ця історія увійшла до фольклору фізиків як «Ньютонівська помста фізики частинок» та приклад зв'язку найпростіших та найстаріших фізичних законів із найбільш передовими науковими концепціями.

Змивання в туалеті зупиняє поїзд

Найкращий апаратний баг, про який я чув, був у швидкісному поїзді до Франції. Баг приводив до аварійного гальмування потягу, але тільки якщо на борту були пасажири. При кожному такому випадку поїзд виводили з експлуатації, перевіряли, але нічого не знаходили. Потім його знову відправляли на лінію, і він одразу аварійно зупинявся.

Під час однієї з перевірок інженер, який їхав поїздом, пішов у туалет. Незабаром він змив за собою, БУМ! Аварійна зупинка.

Інженер зв'язався з машиністом і запитав:

— Що ти робив перед гальмуванням?

— Ну, я пригальмовував на узвозі.

Це було дивно, бо за звичайного курсування поїзд пригальмовує на спусках десятки разів. Склад вирушив далі, і наступного спуску машиніст попередив:

— Я збираюся пригальмовувати.

Нічого не трапилося.

— Що ти робив під час останнього гальмування? — спитав машиніст.

— Ну… я був у туалеті…

— Ну, тоді йди в туалет і зроби те, що робив, коли спускатимемося знову!

Інженер вирушив у туалет, і коли машиніст попередив: "Я гальмую", він спустив воду. Звичайно ж, поїзд відразу зупинився.

Тепер вони могли відтворити проблему, і потрібно було знайти причину.

Через дві хвилини вони помітили, що кабель дистанційного керування гальмуванням двигуна (у поїзда було по одному двигуну в обох кінцях) від'єднаний від стінки електрошафи і лежить на реле, що управляє соленоїдом туалетної заглушки. захисту від збоїв просто включала аварійне гальмування.

Шлюз, який ненавидів FORTRAN

Кілька місяців тому ми помітили, що мережні підключення до мережі на материку [справа була на Гаваях] ставали дуже повільними. Це могло тривати 10-15 хвилин, а потім зненацька виникало знову. Через деякий час мій колега поскаржився мені, що підключення до мережі на материку взагалі не працюють. Він мав якийсь код на FORTRAN, який потрібно було скопіювати на машину на материку, але це не виходило, тому що «мережа не трималася досить довго, щоб завершилося завантаження по FTP».

Так, виходило так, що відмови мережі виникали тоді, коли колега намагався передати FTP файл з вихідним кодом на FORTRAN на машину на материку. Ми спробували архівувати файл: тоді він спокійно копіювався (але на цільовій машині не було розпакувальника, тому проблема не була вирішена). Нарешті, ми «розділили» код на FORTRAN на дуже маленькі фрагменти і відправили їх по черзі. Більшість фрагментів скопіювали без проблем, але кілька штук не пройшли, або пройшли після численних спроб.

Вивчивши проблемні фрагменти, ми виявили, що у них є дещо спільне: всі вони містять блоки коментарів, які починаються і закінчуються рядками, що складаються з великих літер С (так колега вважав за краще коментувати на FORTRAN). Ми надіслали на материк електронні листи фахівцям з мереж та попросили про допомогу. Звичайно, їм захотілося побачити зразки наших файлів, які не піддаються пересиланню FTP… але наші листи до них не дійшли. Нарешті, ми придумали просто описати, як виглядають файли, що не пересилаються. Це спрацювало 🙂 [Чи насмілюсь я додати сюди приклад одного з проблемних коментарів на FORTRAN? Напевно, не варто!]

Зрештою, нам вдалося розібратися. Між нашою частиною кампуса та виходом у мережу на материку нещодавно встановили новий шлюз. У нього були ВЕЛИЧЕЗНІ труднощі з передачею пакетів, які містили повторювані фрагменти з великих С! Усього кілька таких пакетів могли зайняти всі ресурси шлюзу і дозволяли пробитися більшості інших пакетів. Ми поскаржилися виробнику шлюзу ... і нам відповіли: «А, так, ви зіткнулися з багом С, що повторюються! Ми вже знаємо про нього». Зрештою, ми вирішили проблему, купивши новий шлюз іншого виробника (на захист першого скажу, що нездатність передавати програми на FORTRAN для когось може виявитися перевагою!).

Важкі часи

Кілька років тому, працюючи над створенням ETL-системи на Perl, призначеної для зниження витрат на третій етап клінічних випробувань, мені потрібно обробити близько 40 000 дат. Дві із них не пройшли перевірку. Мене це не надто стурбувало, бо ці дати були взяті з наданих клієнтом даних, які часто, скажімо так, дивували. Але коли я перевірив вихідні дані, виявилося, що цими датами були 1 січня 2011 і 1 січня 2007 року. Це може звучати таємничо для тих, хто не знайомий із екосистемою програмного забезпечення. Через давнє рішення іншої компанії, прийнятого за заробіток грошей, мій клієнт заплатив мені за виправлення бага, який одна компанія внесла випадково, а інша навмисно. Щоб ви зрозуміли, про що мова, мені потрібно розповісти про компанію, яка додала фічу, яка в результаті стала багом, а також ще про кілька цікавих подій, які зробили внесок у виправлений мною таємничий баг.

У давні часи комп'ютери Apple іноді спонтанно скидали свою дату на 1 січня 1904 року. Причина була простою: для відстеження дати і часу використовувалися працюючі від батарейки «системний годинник». Що відбувалося, коли батарейка сідала? Комп'ютери починали відстежувати дату за кількістю секунд початку епохи. Під епохою малася на увазі референсна вихідна дата, і для Macintosh'ї це було 1 січня 1904 року. І після вмирання батарейки поточна дата скидалася на вказану. Але чому так сталося?

Раніше для зберігання кількості секунд з вихідної дати Apple використала 32 біти. Один біт може зберігати одне з двох значень - 1 або 0. Два біти можуть зберігати одне з чотирьох значень: 00, 01, 10, 11. Три біти - одне значення з восьми: 000, 001, 010, 011, 100, 101, 110, 111 і т.д. А 32 могли зберігати одне з 232 значень, тобто 4 секунд. Для дат за версією Apple це дорівнювало приблизно 294 років, тому старі Маки не можуть обробляти дати після 967 року. І якщо системна батарея сідає, дата скидається на 296 секунд з початку епохи, і доводиться вручну виставляти дату при кожному включенні комп'ютера (або поки ви не купите нову батарею).

Однак рішення Apple зберігати дати у вигляді секунд з початку епохи означало, що ми не можемо обробляти дати до початку епохи, що мало далекосяжні наслідки, як ми побачимо. Apple запровадила фічу, а не баг. Крім іншого, це означало, що операційна система Macintosh була невразливою для «бага міленіуму» (чого не скажеш про багато додатків для Мака, які мали власні системи обчислення дат для обходу обмежень).

Йдемо далі. Ми використовували Lotus 1-2-3, розроблений IBM "кілер-додаток", який допоміг запустити PC-революцію, хоча на Apple-комп'ютерах була VisiCalc, що забезпечила успіх персональним комп'ютерам. Заради справедливості, якби 1-2-3 не з'явилася, PC навряд чи злетіли б, а історія персональних комп'ютерів могла б розвиватися зовсім інакше. Lotus 1-2-3 некоректно обробляла 1900 як високосний рік. Коли Microsoft випустила свою першу електронну таблицю Multiplan, вона зайняла невелику частку ринку. І коли запустили проект Excel, вирішили не тільки скопіювати у Lotus 1-2-3 схему іменування рядків і колонок, а й забезпечити сумісність по багах, свідомо обробляючи 1900-й як високосний рік. Ця проблема існує досі. Тобто в 1-2-3 це було багом, а в Excel - свідомим рішенням, яке гарантувало, що всі користувачі 1-2-3 можуть імпортувати свої таблиці в Excel без змін даних, навіть якщо вони помилкові.

Але тут була ще одна проблема. Спочатку Microsoft випустила Excel для Macintosh, який не визнавав дати до 1 січня 1904 року. А в Excel початком епохи вважалося 1 січня 1900 року. Тому розробники внесли зміну, щоб їхня програма розпізнавала вигляд епохи і зберігала в собі дані відповідно до потрібної епохи. Microsoft навіть написала про це статтю, що пояснює. І це рішення призвело до мого бага.

Моя ETL-система отримувала від покупців Excel-таблиці, які створювалися під Windows, але були створені і Маку. Тому початком доби в таблиці могло бути як 1 січня 1900 року, так і 1 січня 1904 року. Як це дізнатися? Формат файлу Excel показує потрібну інформацію, а парсер, який я застосовував, не показував (тепер показує), і припускав, що ви знаєте епоху для конкретної таблиці. Напевно, можна було витратити більше часу на те, щоб розібратися у двійковому форматі Excel і надіслати патч автору парсера, але мені потрібно було зробити для клієнта багато іншого, тому я швидко написав евристику для визначення епохи. Вона була простою.

В Excel дата 5 липня 1998 може бути представлена ​​у форматі "07-05-98" (марна американська система), "Jul 5, 98", "July 5, 1998", "5-Jul-98" або в якому-небудь іншому марному форматі (за іронією долі, одним із форматів, який не пропонувала моя версія Excel, був стандарт ISO 8601). Однак усередині таблиці неформатована дата зберігалася або як "35981" для епохи-1900, або як "34519" для епохи-1904 (числа становлять кількість днів з початку епохи). Я просто за допомогою простого парсера витягував рік відформатованої дати, а потім за допомогою парсера Excel витягував рік з невідформатованої дати. Якщо обидва значення відрізнялися на 4 роки, то я розумів, що використовую систему з епохою-1904.

Чому я не використовував просто відформатовані дати? Тому що 5 липня 1998 року може бути відформатовано як «July, 98» із втратою дня місяця. Ми отримували таблиці від такої кількості компаній, які створювали їх такими різними способами, що розбиратися з датами мали ми (у разі я). Крім того, якщо Excel розуміє правильно, то й ми маємо!

Тоді ж я зіткнувся з 39082. Нагадаю, що Lotus 1-2-3 вважав 1900 високосним, і це сумлінно повторили в Excel. А оскільки це додавало до 1900-го один день, багато функцій обчислення дат могли помилятися на цей день. Тобто 39082 могло бути 1 січня 2011 року (на Маках) або 31 грудня 2006 року (у Windows). Якщо мій «парсер років» витягував з відформатованого значення 2011 рік, то все добре. Але оскільки парсер Excel не знає, яка використовується епоха, він за умовчанням застосовує епоху-1900, повертаючи 2006 рік. Мій додаток бачив, що різниця становить 5 років, вважала це помилкою, журналювала та повертала невідформатоване значення.

Щоб це обійти, я написав ось це (псевдокод):

diff = formatted_year - parsed_year
if 0 == diff
    assume 1900 date system
if 4 == diff
    assume 1904 date system
if 5 == diff and month is December and day is 31
    assume 1904 date system

І тоді всі 40 тисяч дат відпарсувалися коректно.

Серед великих завдань на друк

На початку 1980-х мій батько працював у Storage Technology, у не існуючому нині підрозділі, який створював стрічкові накопичувачі та пневматичні системи для високошвидкісної подачі стрічок.

Вони так переробляли накопичувачі, щоб ті могли мати один центральний привід «А», з'єднаний із сімома приводами «Б», а маленька ОС в оперативній пам'яті, що керувала приводом «А», могла делегувати операції читання та запису з приводу «Б».

При кожному запуску приводу «А» потрібно було вставляти дискету в периферійний дисковод, підключений до «А», щоб завантажити його пам'ять операційну систему. Вона була дуже примітивною: обчислювальна потужність забезпечувалася 8-бітним мікроконтролером.

Цільовою аудиторією такого обладнання були компанії з дуже великими сховищами даних — банки, роздрібні мережі тощо — яким потрібно було друкувати багато ярликів із адресами чи банківських виписок.

Один клієнт мав проблему. Серед завдання друку один конкретний привід «А» міг перестати працювати, через що вставало все завдання. Щоб поновити роботу приводу, персоналу доводилося все перезавантажувати. І якщо це відбувалося серед шестигодинного завдання, то губилося безліч дорогого комп'ютерного часу і зривалося розклад всієї операції.

Із Storage Technologies відправили техніків. Але незважаючи на всі спроби, вони не змогли відтворити баг у тестових умовах: схоже, збій з'являвся серед високих завдань на друк. Проблема була не в обладнанні, вони замінили все, що могли: оперативну пам'ять, мікроконтролер, дисковод, усі можливі частини стрічкового приводу – проблема зберігалася.

Тоді техніки зателефонували до штаб-квартири та покликали Експерта.

Експерт взяв крісло і чашку кави, сів у комп'ютерній залі — на той час були кімнати, спеціально виділені під комп'ютери — і дивився, як персонал ставить у чергу велике завдання на друк. Експерт чекав, щоб стався збій, і той стався. Усі подивилися на Експерта — а той не мав жодних ідей, чому так сталося. Тому він наказав знову поставити завдання у чергу, і всі співробітники з техніками повернулися до роботи.

Експерт знову сів у крісло і почав чекати на збій. Минуло близько шостої години, і збій стався. Експерт знову не мав ідей, крім того, що все трапилося в заповненому людьми приміщенні. Він наказав перезапустити завдання, знову сів і почав чекати.

До третього збою Експерт дещо помітив. Збій відбувався тоді, коли персонал змінював стрічки у сторонньому приводі. Більше того, збій виникав, як тільки один із співробітників проходив через певну плитку на підлозі.

Фальшпідлога була зроблена з алюмінієвих плиток, укладених на висоті 6-8 дюймів. Під фальшпідлогою проходили численні дроти від комп'ютерів, щоб хтось випадково не настав на важливий кабель. Плитки були покладені дуже щільно, щоб під фальшпідлогу не потрапляло сміття.

Експерт зрозумів, що одну з плиток було деформовано. Коли співробітник наступав на її кут, плитка терлася краями об сусідні плитки. З ними терлися і пластмасові деталі, що з'єднували плитки, через що виникали статичні мікророзряди, що створювали радіочастотні перешкоди.

Сьогодні оперативна пам'ять набагато краще захищена від радіочастотних перешкод. Але в ті роки це було не так. Експерт зрозумів, що ці перешкоди порушували роботу пам'яті, а з нею й операційну систему. Він зателефонував до служби супроводу, замовив нову плитку, сам її встановив, і проблема зникла.

Це приплив!

Історія сталася в серверній кімнаті, на четвертому або п'ятому поверсі офісу в Портсмуті (здається), в районі доків.

Одного разу впав Unix-сервер із основною базою даних. Його перезавантажували, але він радісно продовжував щоразу падати. Вирішили покликати когось із служби підтримки.

Чувак із підтримки… здається, його звали Марк, але це не важливо… навряд чи я з ним знайомий. Це не має значення, правда. Зупинимося на "Марці", добре? Чудово.

Отже, за кілька годин прибув Марк (від Лідса до Портсмута шлях не близький, чи знаєте), увімкнув сервер і все запрацювало без проблем. Типова чортова підтримка, клієнт через це дуже засмучується. Марк переглядає файли журналу і не знаходить нічого поганого. Тоді Марк повертається на поїзд (або на якому там виді транспорту він приїхав, це могла бути і кульгава корова, наскільки я знаю… коротше, це не важливо, гаразд?) і вирушає назад до Лідса, марно витративши день.

Того ж вечора сервер падає знову. Історія та сама… сервер не піднімається. Марк віддалено намагається допомогти, але клієнт не може запустити сервер.

Знову поїзд, автобус, лимонна безе або ще якась херня, і Марк знову у Портсмуті. Глянь-но, сервер завантажується без проблем! Чудо. Марк кілька годин перевіряє, що з операційною або ПЗ все гаразд, і вирушає до Лідсу.

Приблизно в середині дня сервер падає (легше!). На цей раз здається розумним залучити людей з апаратної підтримки, щоб вони замінили сервер. Але ні, приблизно за 10 годин він теж падає.

Ситуація повторювалася кілька днів. Сервер працює приблизно через 10 годин падає і не запускається протягом наступних 2 годин. Вони перевірили охолодження, витоку пам'яті, вони перевірили все, але нічого не знайшли. Потім збої припинилися.

Тиждень минув безтурботно... всі були щасливі. Щасливі, поки не почалося знову. Картина та сама. 10 годин роботи, 2-3 години простою.

А потім хтось (здається, мені казали, що ця людина не мала відношення до ІТ) сказав:

«Це приплив!»

Вигук зустріли порожніми поглядами, і, мабуть, чиясь рука завагалася біля кнопки виклику охорони.

"Він перестає працювати з припливом".

Здавалося б, це зовсім чужа концепція для співробітників ІТ-підтримки, які навряд чи читають щорічник припливів, сидячи за кавою. Вони пояснили, що це не може бути пов'язане з припливом, тому що сервер працював тиждень без збоїв.

«Минулого тижня приплив був низьким, а цього високого».

Трохи термінології для тих, хто не має ліцензії на керування яхтою. Припливи залежить від місячного циклу. І в міру обертання Землі, кожні 12,5 годин гравітаційне тяжіння Сонця і Місяця створює приливну хвилю. На початку 12,5-годинного циклу виникає приплив, у середині циклу - відплив, а наприкінці знову приплив. Але оскільки орбіта Місяця змінюється, змінюється різниця між відливом і припливом. Коли Місяць знаходиться між Сонцем і Землею або з протилежного боку від Землі (повнолуння чи відсутність Місяця), ми отримуємо сизігійські припливи - найвищі припливи та найнижчі відливи. У півмісяця ми отримуємо квадратурні припливи - найнижчі припливи. Різниця між двома екстремумами сильно зменшується. Місячний цикл триває 28 днів: сизігійські – квадратурні – сизігійські – квадратурні.

Коли технарям пояснили суть припливних сил, ті одразу ж подумали про те, що треба зателефонувати до поліції. І цілком логічно. Але виявилося, що чувак мав рацію. За два тижні до цього неподалік офісу пришвартувався есмінець. Щоразу, коли приплив піднімав його певну висоту, радарний пост корабля опинявся лише на рівні підлоги серверної. І радар (чи засіб РЕБ, чи якась інша іграшка військових) влаштовував у комп'ютерах хаос.

Політне завдання для ракети

Мені доручили портувати велику (близько 400 тис. рядків) систему управління та контролю запуску ракет під нові версії операційної системи, компілятора та мови. Точніше, з Solaris 2.5.1 на Solaris 7, і з Verdix Ada Development System (VADS), написаною на Ada 83, на систему Rational Apex Ada, написану на Ada 95. VADS була куплена компанією Rational, і її продукт застарів, хоча Rational постаралася реалізувати сумісні версії специфічних для пакетів VADS, щоб полегшити перехід на компілятор Apex.

Троє людей допомагали мені просто отримати чисто скомпільований код. На це пішло два тижні. А потім я самостійно працював над тим, щоб змусити систему працювати. Коротше, це були найгірші архітектура та реалізація програмної системи, що мені зустрічалися, тому на завершення портування пішло ще два місяці. Потім систему передали на тестування, що зайняло ще кілька місяців. Я відразу виправляв баги, які знаходили при тестуванні, але їхня кількість швидко знизилася (вихідний код був production-системою, тому його функціональність працювала досить надійно, мені лише довелося прибрати баги, що виникли при адаптації під новий компілятор). Зрештою, коли все працювало так, як і мало, мене перевели на інший проект.

А в п'ятницю перед Днем подяки пролунав телефонний дзвінок.

Приблизно через три тижні мали протестувати запуск ракети, і в ході лабораторних випробувань зворотного відліку послідовність команд виявилася заблокованою. У реальному житті це призвело б до переривання випробувань, а якщо блокування виникло б протягом декількох секунд після запуску двигуна, то у допоміжних системах відбулося б кілька незворотних дій, через які довелося б довго і дорого заново готувати ракету. Вона не стартувала б, але дуже багато людей сильно засмутилося б через втрату часу і дуже великих грошей. Не дозволяйте нікому говорити вам, що міністерство оборони безцеремонно витрачає гроші — я ще не зустрічав жодного менеджера з контрактної діяльності, для якого бюджет не був би на першому чи другому місці, а слідом за ним графік.

У попередні місяці це випробування зворотного відліку проганялося сотні разів у багатьох випадках, і було лише кілька дрібних заминок. Отже, ймовірність виникнення цієї була дуже низькою, проте її наслідки були дуже значними. Перемножте обидва ці фактори, і зрозумійте, що новина передрікала мені і десяткам інженерів і менеджерів зіпсований святковий тиждень.

І увага була звернена на мене, як на людину, яка портувала систему.

Як і в більшості систем, для яких критично важлива безпека, тут журналувалася безліч параметрів, тому вдалося досить легко визначити кілька рядків коду, виконаних перед зависанням системи. І звичайно ж, у них не було абсолютно нічого незвичайного, ті ж вирази успішно виконувалися буквально тисячі разів в ході того ж прогону.

Ми покликали в Rational людей з Apex, оскільки вони розробили компілятор і в підозрілому коді викликалися деякі розроблені ними підпрограми. На них (і всіх інших) справило враження, що слід з'ясувати причину проблеми буквально національного значення.

Оскільки в журналах не було нічого цікавого, то вирішили спробувати відтворити проблему в місцевій лабораторії. Це було непросте завдання, оскільки подія виникала приблизно один раз на 1000 прогонів. Однією з ймовірних причин було те, що виклик розробленої постачальником м'ютекс-функції (частина пакету міграції VADS) Unlock не призводив до розблокування. Потік обробки, що викликав функцію, обробляв heartbeat-повідомлення, які номінально надходили кожну секунду. Ми підняли частоту до 10 Гц, тобто 10 разів на секунду, і почали прогін. Приблизно за годину система заблокувалася. У журналі ми побачили, що послідовність записаних повідомлень була такою самою, як під час збійного випробування. Зробили ще кілька прогонів, система стабільно блокувалася через 45—90 хвилин після початку, і щоразу в журналі була та сама траса. Незважаючи на те, що технічно ми зараз виконували інший код — частота повідомлень була іншою — поведінка системи повторювалася, тому ми переконалися, що цей сценарій навантаження приводив до тієї ж проблеми.

Тепер потрібно було з'ясувати, де саме у послідовності виразів виникало блокування.

У цій реалізації системи використовувалася система завдань Ada, і використовувалась неймовірно погано. Завдання - це високорівнева паралельно виконувана конструкція в Ada, щось на зразок потоків виконання, тільки вбудована в саму мову. Коли двом завданням потрібно взаємодіяти, вони «призначають рандеву» (rendezvous), обмінюються потрібними даними, а потім припиняють рандеву та повертаються до своїх незалежних виконань. Проте систему було реалізовано інакше. Після того, як з цільовим завданням проводилося рандеву, це цільове завдання проводило рандеву з іншим завданням, яке потім проводило рандеву з третьою, і так далі, доки не завершувалася якась обробка. Після цього всі ці рандеву завершувалися і кожна задача мала повернутися до свого виконання. Тобто ми мали справу з найдорожчою у світі системою виклику функцій, яка стопорила весь «багатозадачний» процес, поки обробляла частину вхідних даних. І раніше це не призводило до проблем лише тому, що пропускна спроможність була дуже низькою.

Я описав цей механізм завдань тому, коли запитувалося рандеву чи очікувалося його завершення, могло відбуватися «перемикання завдань». Тобто процесор міг розпочати обробку іншого завдання, готового до виконання. Виходить, що коли одна задача готова до рандева з іншим завданням, може початися виконання зовсім іншого завдання, і врешті-решт управління повертається першому рандеву. І можуть виникати інші події, що призводять до перемикання завдання; одна з таких подій - виклик системної функції, наприклад, друк або виконання м'ютексу.

Щоб зрозуміти, який рядок коду призводив до проблеми, мені потрібно було знайти спосіб записувати прогрес проходження послідовності виразів, при цьому не ініціюючи перемикання завдання, що може завадити виникненню збою. Тому я не міг скористатися Put_Line(), щоб не виконувати операції вводу-виводу. Можна було задати змінну-лічильник чи щось подібне, але як мені подивитися її значення, якщо я не можу виводити її на екран?

Також при вивченні журналу з'ясувалося, що, незважаючи на зависання обробки heartbeat-повідомлень, яке блокувало всі операції введення-виведення процесу і не давало виконувати інші обробки, інші незалежні завдання продовжували виконуватися. Тобто робота блокувалася не повністю, лише (критичний) ланцюжок завдань.

Це була зачіпка, необхідна обчислення блокуючого висловлювання.

Я зробив Ada-пакет, який містив завдання, перерахований тип і глобальну змінну цього типу. Перелічені літерали були прив'язані до конкретних виразів проблемної послідовності (наприклад, Incrementing_Buffer_Index, Locking_Mutex, Mutex_Unlocked), а потім вставив у неї вирази присвоєння, які надавали відповідне перерахування глобальної змінної. Оскільки об'єктний код всього цього просто зберігав постійну пам'яті, перемикання завдання в результаті його виконання було вкрай малоймовірним. Насамперед ми підозрювали висловлювання, які могли переключити завдання, оскільки блокування виникало під час виконання, а чи не повернення при зворотному перемиканні завдання (з кількох причин).

Відстеження завдання просто виконувалася в циклі і періодично перевіряла, чи змінилося значення глобальної змінної. При кожній зміні значення зберігалося у файл. Потім недовге очікування та нова перевірка. Я записував змінну файл тому, що завдання виконувалася тільки тоді, коли система вибирала її для виконання при перемиканні завдання в проблемній області. Хоч би що відбувалося у цій задачі, це не впливало б на інші, не пов'язані з нею заблоковані завдання.

Очікувалося, що коли система дійде до виконання проблемного коду, глобальна змінна скидатиметься при переході до кожного наступного виразу. Потім відбудеться щось, що призводить до перемикання задачі, і оскільки частота його виконання (10 Гц) нижче, ніж у моніторі задачі, монітор міг зафіксувати значення глобальної змінної і записати його. У нормальній ситуації я міг би отримати послідовність підмножини перерахувань, що повторюється: останні значення змінної на момент перемикання задачі. При зависанні глобальна змінна має більше змінюватися, і останнє записане значення покаже, виконання якого висловлювання завершилося.

Запустив код із відстеженням. Він завис. А моніторинг спрацював як по маслу.

У журналі виявилася очікувана послідовність, яка переривалася значенням, що вказує на те, що був викликаний м'ютекс Unlock, а завдання не завершено — як і у випадку із тисячами попередніх викликів.

Інженери з Apex у цей час гарячково аналізували свій код і знайшли в м'ютексі місце, де теоретично могло виникати блокування. Але її ймовірність була дуже мала, оскільки до блокування могла призвести лише певна послідовність подій, що відбуваються у певний час. Закон Мерфі, хлопці, це закон Мерфі.

Щоб захистити потрібний фрагмент коду, я замінив виклики м'ютекс-функцій (побудованих на основі м'ютекс-функціональності ОС) маленьким нативним пакетом м'ютекс-Ada, щоб з його допомогою керувати доступом м'ютексів до цього фрагменту.

Вставив у код і запустив тест. Через сім годин код продовжував працювати.

Мій код передали в Rational, там його скомпілювали, дизассемблювали і перевірили, що в ньому не використовується той самий підхід, який застосовувався у проблемних м'ютекс-функціях.

Це була багатолюдна перевірка коду в моїй кар'єрі 🙂 У кімнаті зі мною було близько десяти інженерів і менеджерів, ще десяток людей підключилися по конференц-дзвінку - і всі вони досліджували близько 20 рядків коду.

Код пройшов перевірку, зібрали нові файли, що виконуються, і передали на формальне регресійне тестування. Через кілька тижнів випробування зворотного відліку пройшли успішно, і ракета злетіла.

Гаразд, це все добре і чудово, але в чому суть цієї історії?

Це була абсолютно огидна проблема. Сотні тисяч рядків коду, паралельне виконання, понад десяток взаємодіючих процесів, погана архітектура та погана реалізація, інтерфейси для вбудованої системи та мільйони витрачених доларів. Жодного тиску, вірно.

Я був не єдиним, хто працював над цією проблемою, хоч і перебував у центрі уваги, оскільки робив портування. Але хоча я й робив його, це не означає, що я розбирався з усіма сотнями тисяч рядків коду, або хоча б швидко переглянув їх. Код і журнали аналізували інженери по всій країні, але коли вони говорили мені свої гіпотези про причини збою, мені вистачало півхвилини, щоб спростувати їх. І коли мене просили проаналізувати теорії, я перекладав це на когось іншого, бо мені було очевидно, що ці інженери йдуть не тим шляхом. Звучить самовпевнено? Так, це так, але я відхилив гіпотези та запити з іншої причини.

Я розумів природу проблеми. Я не знав точно, де вона виникає і чому, але я знав, що саме відбувається.

За роки роботи я накопичив багато знань та досвіду. Я був одним з піонерів використання Ada, розумів її переваги та недоліки. Я знаю, як runtime-бібліотеки Ada обробляють завдання та працюють з паралельним виконанням. І я розбираюся в низькорівневому програмуванні на рівні пам'яті, регістрів та асемблера. Іншими словами, у мене глибокі знання у своїй сфері. І я використав їх для пошуку причини проблеми. Я не просто обійшов баг, а розумів, як знайти його в умовах дуже чутливого середовища виконання.

Такі історії боротьби з кодом не надто цікаві для тих, хто не знайомий з особливостями та умовами такої боротьби. Але ці історії допомагають зрозуміти, що потрібно для вирішення справді важких проблем.

Для вирішення справді важких проблем вам потрібно бути не просто програмістом. Вам потрібно розуміти "долю" коду, як він взаємодіє зі своїм оточенням і як працює саме оточення.

І тоді у вас буде свій зіпсований святковий тиждень.

Далі буде.

Джерело: habr.com

Додати коментар або відгук