Skip to content

Commit 7a2e7e0

Browse files
committed
System Touch
1 parent 2e3998f commit 7a2e7e0

3 files changed

Lines changed: 217 additions & 70 deletions

File tree

source/db/N21DataSource.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,28 @@
55

66
/**
77
* Provides a single shared JDBC connection to the N21 MySQL database.
8-
* Reconnects automatically if the connection is closed/stale.
8+
* Once a connection attempt fails, isAvailable() returns false so callers
9+
* can route immediately to the XML fallback without retrying on every call.
10+
* A successful reconnect re-enables DB mode automatically.
911
*/
1012
public class N21DataSource
1113
{
12-
private static final String URL = "jdbc:mysql://localhost:3306/N21?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
14+
private static final String URL = "jdbc:mysql://localhost:3306/N21?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&connectTimeout=3000";
1315
private static final String USER = "root";
14-
private static final String PASS = ""; // set password here or via env N21_DB_PASS
16+
private static final String PASS = ""; // override with env N21_DB_PASS
1517

16-
private static Connection CONNECTION = null;
18+
private static Connection CONNECTION = null;
19+
private static boolean DB_FAILED = false; // latched on first failure, cleared on success
20+
21+
public static synchronized boolean isAvailable()
22+
{
23+
if (DB_FAILED) return false;
24+
try
25+
{
26+
return CONNECTION != null && !CONNECTION.isClosed();
27+
}
28+
catch (Exception e) { return false; }
29+
}
1730

1831
public static synchronized Connection get() throws Exception
1932
{
@@ -24,14 +37,24 @@ public static synchronized Connection get() throws Exception
2437
Class.forName("com.mysql.cj.jdbc.Driver");
2538
CONNECTION = DriverManager.getConnection(URL, USER, pass);
2639
CONNECTION.setAutoCommit(true);
40+
DB_FAILED = false; // successful connect clears failure latch
2741
}
2842

2943
return CONNECTION;
3044
}
3145

46+
/** Mark DB as unavailable — called by N21Store on any exception. */
47+
public static synchronized void markFailed()
48+
{
49+
DB_FAILED = true;
50+
try { if (CONNECTION != null) CONNECTION.close(); } catch (Exception ignored) {}
51+
CONNECTION = null;
52+
}
53+
3254
public static synchronized void close()
3355
{
3456
try { if (CONNECTION != null) CONNECTION.close(); } catch (Exception ignored) {}
3557
CONNECTION = null;
58+
DB_FAILED = false;
3659
}
3760
}

source/db/N21Store.java

Lines changed: 129 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,122 +5,185 @@
55

66
import java.sql.PreparedStatement;
77
import java.sql.Timestamp;
8-
import java.time.Instant;
9-
import java.time.LocalTime;
108

119
/**
1210
* Static store methods — one per N21 table.
13-
* Each method silently swallows failures so the server never crashes on a DB write.
11+
* Each method attempts MySQL first; on any failure it marks the DB unavailable
12+
* and seamlessly routes the record to the XML fallback.
1413
*/
1514
public class N21Store
1615
{
17-
// ── connections ──────────────────────────────────────────────────────────
16+
// ── connections ──────────────────────────────────────────────────────────
1817

1918
public static void storeConnection(Connection c, int serverPort)
2019
{
21-
try
20+
String remoteAddr = c.remote_address != null ? c.remote_address : "";
21+
String inetAddr = c.internet_address != null ? c.internet_address.getHostAddress() : "";
22+
String telnet = Boolean.TRUE.equals(c.IS_TELNET_EXCELSIOR_CONNECTED) ? "1" : "0";
23+
String inception = c.inception_date != null ? c.inception_date.toString() : "";
24+
25+
if (dbOk())
2226
{
23-
PreparedStatement ps = N21DataSource.get().prepareStatement(
24-
"INSERT INTO connections (remote_address, internet_address, server_port, is_telnet_excelsior_connected, inception_date) VALUES (?,?,?,?,?)");
25-
ps.setString(1, c.remote_address != null ? c.remote_address : "");
26-
ps.setString(2, c.internet_address != null ? c.internet_address.getHostAddress() : "");
27-
ps.setInt(3, serverPort);
28-
ps.setBoolean(4, Boolean.TRUE.equals(c.IS_TELNET_EXCELSIOR_CONNECTED));
29-
ps.setTimestamp(5, c.inception_date != null ? new Timestamp(c.inception_date.getTime()) : new Timestamp(System.currentTimeMillis()));
30-
ps.executeUpdate();
31-
ps.close();
27+
try
28+
{
29+
PreparedStatement ps = N21DataSource.get().prepareStatement(
30+
"INSERT INTO connections (remote_address, internet_address, server_port, is_telnet_excelsior_connected, inception_date) VALUES (?,?,?,?,?)");
31+
ps.setString(1, remoteAddr);
32+
ps.setString(2, inetAddr);
33+
ps.setInt(3, serverPort);
34+
ps.setBoolean(4, Boolean.TRUE.equals(c.IS_TELNET_EXCELSIOR_CONNECTED));
35+
ps.setTimestamp(5, c.inception_date != null ? new Timestamp(c.inception_date.getTime()) : new Timestamp(System.currentTimeMillis()));
36+
ps.executeUpdate(); ps.close();
37+
return;
38+
}
39+
catch (Exception e) { fail("connections", e); }
3240
}
33-
catch (Exception e) { System.err.println("[N21Store] connections: " + e.getMessage()); }
41+
N21XmlFallback.append("connections",
42+
"remote_address", remoteAddr, "internet_address", inetAddr,
43+
"server_port", String.valueOf(serverPort), "telnet", telnet, "inception_date", inception);
3444
}
3545

3646
// ── geo_locations ─────────────────────────────────────────────────────────
3747

3848
public static void storeGeo(String ip, String city, String country)
3949
{
40-
try
50+
if (dbOk())
4151
{
42-
PreparedStatement ps = N21DataSource.get().prepareStatement(
43-
"INSERT INTO geo_locations (ip_address, city, country) VALUES (?,?,?) " +
44-
"ON DUPLICATE KEY UPDATE city=VALUES(city), country=VALUES(country), resolved_at=NOW()");
45-
ps.setString(1, ip);
46-
ps.setString(2, city != null ? city : "");
47-
ps.setString(3, country != null ? country : "");
48-
ps.executeUpdate();
49-
ps.close();
52+
try
53+
{
54+
PreparedStatement ps = N21DataSource.get().prepareStatement(
55+
"INSERT INTO geo_locations (ip_address, city, country) VALUES (?,?,?) " +
56+
"ON DUPLICATE KEY UPDATE city=VALUES(city), country=VALUES(country), resolved_at=NOW()");
57+
ps.setString(1, ip); ps.setString(2, city != null ? city : ""); ps.setString(3, country != null ? country : "");
58+
ps.executeUpdate(); ps.close();
59+
return;
60+
}
61+
catch (Exception e) { fail("geo_locations", e); }
5062
}
51-
catch (Exception e) { System.err.println("[N21Store] geo_locations: " + e.getMessage()); }
63+
N21XmlFallback.append("geo_locations", "ip_address", ip, "city", city, "country", country);
5264
}
5365

5466
// ── exceptions ────────────────────────────────────────────────────────────
5567

5668
public static void storeException(ExceptionRecord r, boolean isSecurityEvent)
5769
{
58-
try
70+
if (dbOk())
5971
{
60-
PreparedStatement ps = N21DataSource.get().prepareStatement(
61-
"INSERT INTO exceptions (exception_type, message, origin, stack_trace, is_security_event, recorded_at) VALUES (?,?,?,?,?,?)");
62-
ps.setString(1, r.exception().getClass().getSimpleName());
63-
ps.setString(2, r.exception().getMessage());
64-
ps.setString(3, r.origin());
65-
ps.setString(4, r.stackTrace());
66-
ps.setBoolean(5, isSecurityEvent);
67-
ps.setTimestamp(6, Timestamp.from(r.timestamp()));
68-
ps.executeUpdate();
69-
ps.close();
72+
try
73+
{
74+
PreparedStatement ps = N21DataSource.get().prepareStatement(
75+
"INSERT INTO exceptions (exception_type, message, origin, stack_trace, is_security_event, recorded_at) VALUES (?,?,?,?,?,?)");
76+
ps.setString(1, r.exception().getClass().getSimpleName());
77+
ps.setString(2, r.exception().getMessage());
78+
ps.setString(3, r.origin());
79+
ps.setString(4, r.stackTrace());
80+
ps.setBoolean(5, isSecurityEvent);
81+
ps.setTimestamp(6, Timestamp.from(r.timestamp()));
82+
ps.executeUpdate(); ps.close();
83+
return;
84+
}
85+
catch (Exception e) { fail("exceptions", e); }
7086
}
71-
catch (Exception e) { System.err.println("[N21Store] exceptions: " + e.getMessage()); }
87+
N21XmlFallback.append("exceptions",
88+
"exception_type", r.exception().getClass().getSimpleName(),
89+
"message", r.exception().getMessage(),
90+
"origin", r.origin(),
91+
"stack_trace", r.stackTrace(),
92+
"security", String.valueOf(isSecurityEvent),
93+
"recorded_at", r.timestamp().toString());
7294
}
7395

7496
// ── security_events ───────────────────────────────────────────────────────
7597

7698
public static void storeSecurityEvent(ExceptionRecord r, String sourceIp)
7799
{
78-
try
100+
if (dbOk())
79101
{
80-
PreparedStatement ps = N21DataSource.get().prepareStatement(
81-
"INSERT INTO security_events (event_type, message, origin, source_ip, recorded_at) VALUES (?,?,?,?,?)");
82-
ps.setString(1, r.exception().getClass().getSimpleName());
83-
ps.setString(2, r.exception().getMessage());
84-
ps.setString(3, r.origin());
85-
ps.setString(4, sourceIp != null ? sourceIp : "");
86-
ps.setTimestamp(5, Timestamp.from(r.timestamp()));
87-
ps.executeUpdate();
88-
ps.close();
102+
try
103+
{
104+
PreparedStatement ps = N21DataSource.get().prepareStatement(
105+
"INSERT INTO security_events (event_type, message, origin, source_ip, recorded_at) VALUES (?,?,?,?,?)");
106+
ps.setString(1, r.exception().getClass().getSimpleName());
107+
ps.setString(2, r.exception().getMessage());
108+
ps.setString(3, r.origin());
109+
ps.setString(4, sourceIp != null ? sourceIp : "");
110+
ps.setTimestamp(5, Timestamp.from(r.timestamp()));
111+
ps.executeUpdate(); ps.close();
112+
return;
113+
}
114+
catch (Exception e) { fail("security_events", e); }
89115
}
90-
catch (Exception e) { System.err.println("[N21Store] security_events: " + e.getMessage()); }
116+
N21XmlFallback.append("security_events",
117+
"event_type", r.exception().getClass().getSimpleName(),
118+
"message", r.exception().getMessage(),
119+
"origin", r.origin(),
120+
"source_ip", sourceIp != null ? sourceIp : "",
121+
"recorded_at", r.timestamp().toString());
91122
}
92123

93124
// ── national_ids ──────────────────────────────────────────────────────────
94125

95126
public static void storeNationalId(long eightDigit, long sixteenDigit)
96127
{
97-
try
128+
if (dbOk())
98129
{
99-
PreparedStatement ps = N21DataSource.get().prepareStatement(
100-
"INSERT IGNORE INTO national_ids (eight_digit_id, sixteen_digit_key) VALUES (?,?)");
101-
ps.setLong(1, eightDigit);
102-
ps.setLong(2, sixteenDigit);
103-
ps.executeUpdate();
104-
ps.close();
130+
try
131+
{
132+
PreparedStatement ps = N21DataSource.get().prepareStatement(
133+
"INSERT IGNORE INTO national_ids (eight_digit_id, sixteen_digit_key) VALUES (?,?)");
134+
ps.setLong(1, eightDigit); ps.setLong(2, sixteenDigit);
135+
ps.executeUpdate(); ps.close();
136+
return;
137+
}
138+
catch (Exception e) { fail("national_ids", e); }
105139
}
106-
catch (Exception e) { System.err.println("[N21Store] national_ids: " + e.getMessage()); }
140+
N21XmlFallback.append("national_ids",
141+
"eight_digit_id", String.valueOf(eightDigit),
142+
"sixteen_digit_key", String.valueOf(sixteenDigit));
107143
}
108144

109145
// ── status_snapshots ──────────────────────────────────────────────────────
110146

111147
public static void storeStatusSnapshot(int activeConnections, long uptimeSecs, long totalMb, long usedMb)
112148
{
113-
try
149+
if (dbOk())
150+
{
151+
try
152+
{
153+
PreparedStatement ps = N21DataSource.get().prepareStatement(
154+
"INSERT INTO status_snapshots (active_connections, server_uptime_secs, total_memory_mb, used_memory_mb, local_server_time) VALUES (?,?,?,?,NOW())");
155+
ps.setInt(1, activeConnections); ps.setLong(2, uptimeSecs);
156+
ps.setLong(3, totalMb); ps.setLong(4, usedMb);
157+
ps.executeUpdate(); ps.close();
158+
return;
159+
}
160+
catch (Exception e) { fail("status_snapshots", e); }
161+
}
162+
N21XmlFallback.append("status_snapshots",
163+
"active_connections", String.valueOf(activeConnections),
164+
"uptime_secs", String.valueOf(uptimeSecs),
165+
"total_memory_mb", String.valueOf(totalMb),
166+
"used_memory_mb", String.valueOf(usedMb));
167+
}
168+
169+
// ── helpers ───────────────────────────────────────────────────────────────
170+
171+
/** Returns true only if a live DB connection can be obtained. */
172+
private static boolean dbOk()
173+
{
174+
if (!N21DataSource.isAvailable())
114175
{
115-
PreparedStatement ps = N21DataSource.get().prepareStatement(
116-
"INSERT INTO status_snapshots (active_connections, server_uptime_secs, total_memory_mb, used_memory_mb, local_server_time) VALUES (?,?,?,?,NOW())");
117-
ps.setInt(1, activeConnections);
118-
ps.setLong(2, uptimeSecs);
119-
ps.setLong(3, totalMb);
120-
ps.setLong(4, usedMb);
121-
ps.executeUpdate();
122-
ps.close();
176+
// Attempt a reconnect once per call — if it throws, stay in fallback mode
177+
try { N21DataSource.get(); return true; }
178+
catch (Exception ignored) { return false; }
123179
}
124-
catch (Exception e) { System.err.println("[N21Store] status_snapshots: " + e.getMessage()); }
180+
return true;
181+
}
182+
183+
/** Log the failure, mark the datasource down, and let the caller fall through to XML. */
184+
private static void fail(String table, Exception e)
185+
{
186+
System.err.println("[N21Store] DB unavailable for table '" + table + "': " + e.getMessage() + " — routing to XML fallback.");
187+
N21DataSource.markFailed();
125188
}
126189
}

source/db/N21XmlFallback.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package db;
2+
3+
import java.io.File;
4+
import java.io.FileWriter;
5+
import java.time.LocalDate;
6+
import java.time.Instant;
7+
8+
/**
9+
* XML fallback store — appends records to db/fallback/YYYY-MM-DD/N21.xml
10+
* when MySQL is unavailable. Thread-safe via synchronized append.
11+
*/
12+
public class N21XmlFallback
13+
{
14+
private static File xmlFile = null;
15+
16+
private static synchronized File file()
17+
{
18+
if (xmlFile == null)
19+
{
20+
File dir = new File("db/fallback/" + LocalDate.now());
21+
dir.mkdirs();
22+
xmlFile = new File(dir, "N21.xml");
23+
24+
// Write root open-tag once if file is new
25+
if (!xmlFile.exists() || xmlFile.length() == 0)
26+
{
27+
write("<N21>\n");
28+
}
29+
}
30+
return xmlFile;
31+
}
32+
33+
public static synchronized void append(String table, String... kvPairs)
34+
{
35+
StringBuilder sb = new StringBuilder();
36+
sb.append(" <record table=\"").append(esc(table)).append("\" ts=\"").append(Instant.now()).append("\">\n");
37+
for (int i = 0; i + 1 < kvPairs.length; i += 2)
38+
sb.append(" <").append(kvPairs[i]).append(">").append(esc(kvPairs[i + 1])).append("</").append(kvPairs[i]).append(">\n");
39+
sb.append(" </record>\n");
40+
write(sb.toString());
41+
}
42+
43+
private static void write(String text)
44+
{
45+
try (FileWriter fw = new FileWriter(file(), true))
46+
{
47+
fw.write(text);
48+
}
49+
catch (Exception e)
50+
{
51+
System.err.println("[N21XmlFallback] write failed: " + e.getMessage());
52+
}
53+
}
54+
55+
/** Minimal XML escaping for attribute and element values. */
56+
private static String esc(String s)
57+
{
58+
if (s == null) return "";
59+
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
60+
}
61+
}

0 commit comments

Comments
 (0)