Pukla veza + dva tiha fallback-a; dokazano sa KILL; i tri sloja tisine u samom logu
Simptom, prijavljivan godinama: ako aplikacija ostane uključena duže od oko 24 sata, posle toga svi ekrani vraćaju prazno — do restarta. Radna hipoteza je bila da garbage collector počisti nešto što ne sme.
Nije GC. Uzrok je pucanje veze sa bazom, plus dva tiha fallback-a koja ga pretvore u tišinu umesto u poruku o grešci.
## ✅ Dokazano eksperimentom Veza aplikacije je ubijena iz GUI klijenta (KILL <id>), i simptom se reprodukovao odmah — prazni ekrani, bez ijedne poruke o grešci. Nije više zaključak iz čitanja koda nego reprodukovan kvar, sa uzrokom i poznatim okidačem.
| korak | dokaz |
|---|---|
| Veza se otvara jednom | DriverManager.getConnection("jdbc:mysql://" + host + "/" + baza, ...) — GMySqlDataLink.java:285, bez autoReconnect |
Statement se pravi jednom, odmah posle | openStatement() :63 → createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE) :149 |
| Ništa nikad ne proverava da li je veza živa | u celom fajlu nema isClosed, isValid, ni ponovnog povezivanja |
| Server sam zatvara neaktivnu vezu | wait_timeout nije zadat u my.ini → MySQL podrazumevano 28800 s = 8 h |
| Greška se guta | executeQuery na SQLException loguje i vrati se normalno (:130-135) |
Posledica: prvi upit posle isteka baci izuzetak, vrati se prazan rezultat, uvoz podataka ne doda ni jedan red → prazan ekran. Ništa se ne povezuje ponovo, pa ostaje pokvareno do restarta.
„Duže od 24 sata" se uklapa — sve iznad 8 sati neaktivnosti.
Ovo je vredelo raspetljati, jer je razlog zašto je hipoteza o GC-u bila razumna: nije bilo nikakvog signala. Kad je logovanje uključeno da uhvati izuzetak, LogFile.txt je ostao 0 bajtova. Traženje uzroka pokazalo je da postoje tri zasebna prekidača, i svaki sam za sebe dovoljan da sve ućuti.
Podrazumevana vrednost LoggingDevice je „bez logovanja". Ispravna vrednost za fajl je F + tačno dva razmaka (polje je fiksne širine 3).
falseUslovni ispis propušta samo ako je tip događaja „logabilan". U enumu događaja jedini koji je true je ALWAYS — a on se ne koristi nigde. Tip za SQL (DatabaseMySql), kao i Database i Print, svi su false. Za to ne postoji konfiguracioni prekidač — to su konstante u kodu.
Glavni prekidač „loguj sve" postoji kao promenljiva, ali je deklarisan false i nigde se ne postavlja. Mrtav kod.
Praktična podela: 25 poziva je bezuslovno, 39 je filtrirano. Srećom, ono što je potrebno za dijagnostiku je u bezuslovnim:
| šta | prolazi bez izmene koda? |
|---|---|
greške baze (SQLException + tekst upita) | da |
merenja SortTime: i Diff+: | da (uz testSpeed = true) |
| svaki SQL upit | ne — traži DatabaseMySql → true u izvoru + rebuild |
Ovo je najvažniji deo, i najopasniji:
APPEND → svaki start briše prethodni logwrite(...), a flush() se ne poziva nigdePosledica: sadržaj stoji u baferu i vidi se tek posle urednog izlaska iz aplikacije. Ako aplikacija padne, bude ubijena ili zatvorena preko Task Manager-a — log je prazan.
Dakle dijagnostički kanal ne preživi upravo onaj kvar zbog kojeg bi se čitao. To objašnjava i zašto je raniji „tihi pad" bio tako težak za dijagnozu.
Izađi iz aplikacije kroz meni ili zatvaranje prozora, ne ubijanjem procesa — pa čitaj LogFile.txt. I prepiši ga u stranu pre sledećeg pokretanja, jer start briše.
StandardOpenOption.CREATE + StandardOpenOption.APPENDflush() posle svakog reda (ili PrintWriter sa autoflush) — sadržaj vidljiv dok program radiGDbTable: ako dobavljanje aktivne baze baci izuzetak → tip pada na MEMORY. Tabela tiho postane memorijska, bez podataka i bez greške.GMySqlDataLink.executeQuery: if(mStatement == null) return l; → prazan rezultat; a SQLException se loguje i ne propagira.Zato je „ekran se otvori a nema ništa" prvo mesto gde treba proveriti vezu, a ne podatke.
Iz GUI klijenta za bazu, poveži se kao root (za viđenje i ubijanje tuđih sesija treba privilegija PROCESS/SUPER):
-- 1. sve otvorene sesije; TTIME uz COMMAND='Sleep' je brojac do wait_timeout-a
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO, 100) AS POSLEDNJI_UPIT
FROM information_schema.PROCESSLIST
WHERE ID <> CONNECTION_ID() -- ne prikazuj moju sopstvenu sesiju
ORDER BY TIME DESC;
-- 2. ubij vezu aplikacije (DB = ime baze aplikacije)
KILL 123;KILL je podrazumevano KILL CONNECTION — zatvori celu vezu, tačno kao server po wait_timeout-u. (KILL QUERY bi prekinuo samo tekući upit i ne bi reprodukovao kvar.)
Pa klikni ekran u aplikaciji. Rezultat: prazan grid, bez ijedne poruke o grešci. Potvrđeno.
SET GLOBAL wait_timeout = 30; -- vazi samo za NOVE vezePa restartuj aplikaciju, pusti je da stoji 40 sekundi bez klika, i klikni ekran. Bezbedno: ne upisuje se u my.ini i vraća se na podrazumevano pri restartu servisa. Posle: SET GLOBAL wait_timeout = 28800;
APPEND + flush) — bez toga se sve ostalo popravlja naslepo. Najmanja izmena sa najvećim uticajem na svaku buduću dijagnostiku.LoggingDevice=F u gengine.cfg. Ne rešava kvar, ali greške baze od tada prolaze (bezuslovni ispis). Za svaki SQL dodatno treba DatabaseMySql → true u izvoru + rebuild.autoReconnect=true u JDBC URL. Jedna reč. Stari Connector/J to podržava; ograničenje je da ne vraća stanje sesije, pa je to ublažavanje a ne rešenje.executeQuery, na SQLException tipa „veza", ponovo otvoriti vezu i Statement i probati jednom; a stvarni neuspeh prijaviti, ne vratiti prazan rezultat. Pošto sav SQL prolazi kroz to jedno mesto, popravka je lokalna.wait_timeout-a na serveru samo odlaže — pucanje mreže se i dalje dešava.Tiha rezerva je gora od greške. Oba fallback-a su napisana da program „ne padne" — a rezultat je da je godinama davao pogrešan odgovor (prazno) umesto da kaže šta ne radi. Cena nije bila pad nego izgubljeno vreme na pogrešnu hipotezu: sumnjalo se na GC, jer je to bilo jedino što je ostalo kad nema nikakve poruke.
Uz to, isti kod ima ispravan kanal za prijavu (GLog) — samo je isključen podrazumevanom konfiguracijom. Uključivanje logovanja je najjeftiniji dijagnostički potez u celom sistemu.