1+ package com .bencodez .simpleapi .servercomm .mysql ;
2+
3+ import java .sql .Connection ;
4+ import java .sql .PreparedStatement ;
5+ import java .sql .ResultSet ;
6+ import java .sql .SQLException ;
7+ import java .sql .Statement ;
8+ import java .util .ArrayList ;
9+ import java .util .List ;
10+ import java .util .function .Consumer ;
11+
12+ import javax .sql .DataSource ;
13+
14+ /**
15+ * Proxy-side messenger: listens on "proxy-channel" and forwards to servers.
16+ * Reuses dedicated lock, worker, and publisher connections to avoid exhausting
17+ * the pool. Ensures the required table exists on initialization.
18+ */
19+ public class ProxyMessenger {
20+ private static final String PROXY_CHANNEL = "proxy-channel" ;
21+ private final DataSource ds ;
22+ private Connection lockConn ;
23+ private Connection workConn ;
24+ private Connection pubConn ;
25+ private volatile boolean running = true ;
26+ private long lastSeenId = 0 ;
27+
28+ private final Consumer <ProxyMessage > onMessage ;
29+ private String tableName ;
30+
31+ public ProxyMessenger (String tableName , DataSource dataSource , Consumer <ProxyMessage > onMessage )
32+ throws SQLException {
33+ this .ds = dataSource ;
34+ this .onMessage = onMessage ;
35+ this .tableName = tableName ;
36+ ensureSchema (tableName );
37+ initConnections ();
38+ startListener ();
39+ }
40+
41+ private void ensureSchema (String tableName ) throws SQLException {
42+ String ddl = "CREATE TABLE IF NOT EXISTS " + tableName + "_message_queue ("
43+ + "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, " + "source VARCHAR(36) NOT NULL, "
44+ + "destination VARCHAR(36) NOT NULL, " + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
45+ + "payload LONGTEXT NOT NULL, " + "PRIMARY KEY (id), " + "INDEX idx_dest_id (destination, id)"
46+ + ") ENGINE=InnoDB;" ;
47+ try (Connection conn = ds .getConnection (); Statement stmt = conn .createStatement ()) {
48+ stmt .execute (ddl );
49+ }
50+ }
51+
52+ private void initConnections () throws SQLException {
53+ // Open three long-lived connections
54+ this .lockConn = ds .getConnection ();
55+ this .workConn = ds .getConnection ();
56+ this .pubConn = ds .getConnection ();
57+ // Prime the lock to enter wait-loop
58+ if (!acquireLock (lockConn , PROXY_CHANNEL , 10 )) {
59+ throw new IllegalStateException ("Could not acquire proxy-channel lock on startup" );
60+ }
61+ }
62+
63+ private void startListener () {
64+ Thread t = new Thread (() -> {
65+ while (running ) {
66+ try {
67+ if (acquireLock (lockConn , PROXY_CHANNEL , 300 )) {
68+ List <ProxyMessage > batch = fetchBatch ();
69+ batch .forEach (onMessage );
70+ }
71+ } catch (SQLException e ) {
72+ e .printStackTrace ();
73+ reconnectOnError ();
74+ }
75+ }
76+ }, "ProxyMessenger-Listener" );
77+ t .setDaemon (true );
78+ t .start ();
79+ }
80+
81+ private void deleteMessageById (long id ) throws SQLException {
82+ String delSql = "DELETE FROM " + tableName + "_message_queue WHERE id = ?" ;
83+ try (PreparedStatement del = workConn .prepareStatement (delSql )) {
84+ del .setLong (1 , id );
85+ del .executeUpdate ();
86+ }
87+ }
88+
89+ private List <ProxyMessage > fetchBatch () throws SQLException {
90+ String sql = "SELECT id, source, payload FROM " + tableName + "_message_queue "
91+ + "WHERE destination='proxy' AND id > ? ORDER BY id" ;
92+ try (PreparedStatement ps = workConn .prepareStatement (sql )) {
93+ ps .setLong (1 , lastSeenId );
94+ try (ResultSet rs = ps .executeQuery ()) {
95+ List <ProxyMessage > results = new ArrayList <>();
96+ while (rs .next ()) {
97+ long id = rs .getLong ("id" );
98+ String source = rs .getString ("source" );
99+ String payload = rs .getString ("payload" );
100+ results .add (new ProxyMessage (id , source , payload ));
101+ lastSeenId = id ;
102+ // Delete the message after processing
103+ deleteMessageById (id );
104+ }
105+ return results ;
106+ }
107+ }
108+ }
109+
110+ /**
111+ * Sends a message from the proxy to a specific backend server.
112+ */
113+ public synchronized void sendToBackend (String targetServerId , String payload ) throws SQLException {
114+ String insertSql = "INSERT INTO " + tableName
115+ + "_message_queue (source, destination, payload) VALUES ('proxy', ?, ?)" ;
116+ try (PreparedStatement ins = pubConn .prepareStatement (insertSql )) {
117+ ins .setString (1 , targetServerId );
118+ ins .setString (2 , payload );
119+ ins .executeUpdate ();
120+ }
121+ String channel = "backend-channel-" + targetServerId ;
122+ try (PreparedStatement rel = pubConn .prepareStatement ("SELECT RELEASE_LOCK(?)" )) {
123+ rel .setString (1 , channel );
124+ rel .executeQuery ();
125+ }
126+ }
127+
128+ /**
129+ * Called by each backend instance to send a message to this proxy.
130+ */
131+ public synchronized void sendToProxy (String fromServerId , String payload ) throws SQLException {
132+ String insertSql = "INSERT INTO " + tableName
133+ + "_message_queue (source, destination, payload) VALUES (?, 'proxy', ?)" ;
134+ try (PreparedStatement ins = pubConn .prepareStatement (insertSql )) {
135+ ins .setString (1 , fromServerId );
136+ ins .setString (2 , payload );
137+ ins .executeUpdate ();
138+ }
139+ try (PreparedStatement rel = pubConn .prepareStatement ("SELECT RELEASE_LOCK(?)" )) {
140+ rel .setString (1 , PROXY_CHANNEL );
141+ rel .executeQuery ();
142+ }
143+ }
144+
145+ private boolean acquireLock (Connection conn , String name , int timeout ) throws SQLException {
146+ try (PreparedStatement ps = conn .prepareStatement ("SELECT GET_LOCK(?, ?)" )) {
147+ ps .setString (1 , name );
148+ ps .setInt (2 , timeout );
149+ try (ResultSet rs = ps .executeQuery ()) {
150+ return rs .next () && rs .getInt (1 ) == 1 ;
151+ }
152+ }
153+ }
154+
155+ private void reconnectOnError () {
156+ try {
157+ closeQuiet (lockConn );
158+ closeQuiet (workConn );
159+ closeQuiet (pubConn );
160+ initConnections ();
161+ } catch (SQLException e ) {
162+ e .printStackTrace ();
163+ }
164+ }
165+
166+ public void shutdown () {
167+ running = false ;
168+ closeQuiet (lockConn );
169+ closeQuiet (workConn );
170+ closeQuiet (pubConn );
171+ }
172+
173+ private void closeQuiet (Connection c ) {
174+ if (c != null )
175+ try {
176+ c .close ();
177+ } catch (SQLException ignored ) {
178+ }
179+ }
180+
181+ public static class ProxyMessage {
182+ public final long id ;
183+ public final String sourceServerId ;
184+ public final String payload ;
185+
186+ public ProxyMessage (long id , String sourceServerId , String payload ) {
187+ this .id = id ;
188+ this .sourceServerId = sourceServerId ;
189+ this .payload = payload ;
190+ }
191+ }
192+ }
0 commit comments