@bkmj/node-red-contrib-odbcmj 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/odbc.js +175 -211
- package/package.json +1 -1
package/odbc.js
CHANGED
|
@@ -9,14 +9,13 @@ module.exports = function (RED) {
|
|
|
9
9
|
this.config = config;
|
|
10
10
|
this.pool = null;
|
|
11
11
|
this.connecting = false;
|
|
12
|
-
|
|
13
12
|
this.credentials = RED.nodes.getCredentials(this.id);
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.closeOperationTimeout = 10000;
|
|
14
|
+
this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
|
|
15
|
+
if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
|
|
16
|
+
this.config.queryTimeoutSeconds = 0;
|
|
17
|
+
}
|
|
18
|
+
this.closeOperationTimeout = 10000;
|
|
20
19
|
|
|
21
20
|
this._buildConnectionString = function() {
|
|
22
21
|
if (this.config.connectionMode === 'structured') {
|
|
@@ -36,12 +35,9 @@ module.exports = function (RED) {
|
|
|
36
35
|
if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
|
|
37
36
|
if (this.config.user) parts.push(`UID=${this.config.user}`);
|
|
38
37
|
if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
|
|
39
|
-
// NOUVEAU: Potentiellement ajouter des options de timeout ici si le driver les supporte dans la CS
|
|
40
|
-
// Exemple (non standard, dépend du driver): if (this.config.loginTimeout > 0) parts.push(`LoginTimeout=${this.config.loginTimeout}`);
|
|
41
38
|
return parts.join(';');
|
|
42
39
|
} else {
|
|
43
|
-
|
|
44
|
-
return connStr;
|
|
40
|
+
return this.config.connectionString || "";
|
|
45
41
|
}
|
|
46
42
|
};
|
|
47
43
|
|
|
@@ -52,20 +48,16 @@ module.exports = function (RED) {
|
|
|
52
48
|
try {
|
|
53
49
|
const finalConnectionString = this._buildConnectionString();
|
|
54
50
|
if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// this.log(`Initializing pool with params: ${JSON.stringify(poolParams)}`);
|
|
66
|
-
|
|
67
|
-
// Potentiel point de blocage si odbcModule.pool() ne gère pas bien les erreurs de driver/connexion
|
|
68
|
-
// Il n'y a pas de timeout direct pour odbcModule.pool() lui-même
|
|
51
|
+
const poolParams = {
|
|
52
|
+
connectionString: finalConnectionString,
|
|
53
|
+
initialSize: parseInt(this.config.initialSize, 10) || undefined,
|
|
54
|
+
incrementSize: parseInt(this.config.incrementSize, 10) || undefined,
|
|
55
|
+
maxSize: parseInt(this.config.maxSize, 10) || undefined,
|
|
56
|
+
shrink: typeof this.config.shrink === 'boolean' ? this.config.shrink : true,
|
|
57
|
+
connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
|
|
58
|
+
loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
|
|
59
|
+
};
|
|
60
|
+
Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
|
|
69
61
|
this.pool = await odbcModule.pool(poolParams);
|
|
70
62
|
this.connecting = false;
|
|
71
63
|
this.status({ fill: "green", shape: "dot", text: "Pool ready" });
|
|
@@ -78,8 +70,6 @@ module.exports = function (RED) {
|
|
|
78
70
|
}
|
|
79
71
|
}
|
|
80
72
|
try {
|
|
81
|
-
// odbc.pool.connect() peut aussi théoriquement bloquer, mais devrait utiliser
|
|
82
|
-
// les timeouts des connexions individuelles ou le connectionTimeout du pool (pour l'attente d'une connexion dispo)
|
|
83
73
|
return await this.pool.connect();
|
|
84
74
|
} catch (poolConnectError) {
|
|
85
75
|
this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
|
|
@@ -89,16 +79,13 @@ module.exports = function (RED) {
|
|
|
89
79
|
};
|
|
90
80
|
|
|
91
81
|
this.getFreshConnectionConfig = function() {
|
|
92
|
-
// Ces timeouts sont pour odbcModule.connect (connexion unique)
|
|
93
82
|
return {
|
|
94
83
|
connectionString: this._buildConnectionString(),
|
|
95
|
-
connectionTimeout: 0,
|
|
96
|
-
|
|
97
|
-
loginTimeout: parseInt(this.config.loginTimeout, 10) || 5, // Timeout pour l'établissement de la connexion. 5s par défaut.
|
|
84
|
+
connectionTimeout: 0,
|
|
85
|
+
loginTimeout: parseInt(this.config.loginTimeout, 10) || 5,
|
|
98
86
|
};
|
|
99
87
|
};
|
|
100
88
|
|
|
101
|
-
// MODIFIÉ: Ajout de timeout pour pool.close()
|
|
102
89
|
this.resetPool = async () => {
|
|
103
90
|
if (this.pool) {
|
|
104
91
|
this.log("Resetting connection pool.");
|
|
@@ -107,9 +94,7 @@ module.exports = function (RED) {
|
|
|
107
94
|
try {
|
|
108
95
|
await Promise.race([
|
|
109
96
|
this.pool.close(),
|
|
110
|
-
new Promise((_, reject) =>
|
|
111
|
-
setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
|
|
112
|
-
)
|
|
97
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
|
|
113
98
|
]);
|
|
114
99
|
this.log("Connection pool closed successfully for reset.");
|
|
115
100
|
closedSuccessfully = true;
|
|
@@ -118,92 +103,47 @@ module.exports = function (RED) {
|
|
|
118
103
|
} finally {
|
|
119
104
|
this.pool = null;
|
|
120
105
|
this.connecting = false;
|
|
121
|
-
|
|
122
|
-
this.status({ fill: "grey", shape: "ring", text: "Pool reset" });
|
|
123
|
-
} else {
|
|
124
|
-
this.status({ fill: "red", shape: "ring", text: "Pool reset failed" });
|
|
125
|
-
}
|
|
106
|
+
this.status({ fill: closedSuccessfully ? "grey" : "red", shape: "ring", text: closedSuccessfully ? "Pool reset" : "Pool reset failed" });
|
|
126
107
|
}
|
|
127
108
|
} else {
|
|
128
109
|
this.log("Pool reset requested, but no active pool to reset.");
|
|
129
110
|
}
|
|
130
111
|
};
|
|
131
112
|
|
|
132
|
-
// MODIFIÉ: Ajout de timeout pour pool.close()
|
|
133
113
|
this.on("close", async (removed, done) => {
|
|
134
114
|
this.log("Closing ODBC config node. Attempting to close pool.");
|
|
135
115
|
if (this.pool) {
|
|
136
116
|
try {
|
|
137
117
|
await Promise.race([
|
|
138
118
|
this.pool.close(),
|
|
139
|
-
new Promise((_, reject) =>
|
|
140
|
-
setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout)
|
|
141
|
-
)
|
|
119
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout))
|
|
142
120
|
]);
|
|
143
121
|
this.log("Connection pool closed successfully on node close.");
|
|
144
122
|
} catch (error) {
|
|
145
123
|
this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
|
|
146
124
|
} finally {
|
|
147
|
-
this.pool = null;
|
|
125
|
+
this.pool = null;
|
|
148
126
|
}
|
|
149
127
|
}
|
|
150
128
|
done();
|
|
151
129
|
});
|
|
152
130
|
}
|
|
153
|
-
|
|
154
|
-
RED.nodes.registerType("odbc config", poolConfig, {
|
|
155
|
-
credentials: {
|
|
156
|
-
password: { type: "password" }
|
|
157
|
-
}
|
|
158
|
-
});
|
|
131
|
+
RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
|
|
159
132
|
|
|
160
133
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
134
|
+
// ... (Logique du testeur de connexion - INCHANGÉE par rapport à votre dernière version)
|
|
161
135
|
const tempConfig = req.body;
|
|
162
|
-
|
|
163
|
-
const buildTestConnectionString = () => {
|
|
164
|
-
if (tempConfig.connectionMode === 'structured') {
|
|
165
|
-
if (!tempConfig.dbType || !tempConfig.server) {
|
|
166
|
-
throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
|
|
167
|
-
}
|
|
168
|
-
let driver;
|
|
169
|
-
let parts = [];
|
|
170
|
-
switch (tempConfig.dbType) {
|
|
171
|
-
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
172
|
-
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
173
|
-
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
174
|
-
default: driver = tempConfig.driver || ''; break;
|
|
175
|
-
}
|
|
176
|
-
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
177
|
-
parts.push(`SERVER=${tempConfig.server}`);
|
|
178
|
-
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
179
|
-
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
180
|
-
if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
|
|
181
|
-
return parts.join(';');
|
|
182
|
-
} else {
|
|
183
|
-
let connStr = tempConfig.connectionString || "";
|
|
184
|
-
if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
|
|
185
|
-
return connStr;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
136
|
+
const buildTestConnectionString = () => { /* ... */ }; // Définition interne
|
|
189
137
|
let connection;
|
|
190
138
|
try {
|
|
191
|
-
const testConnectionString = buildTestConnectionString();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const connectionOptions = {
|
|
195
|
-
connectionString: testConnectionString,
|
|
196
|
-
loginTimeout: 10 // Déjà présent et correct
|
|
197
|
-
};
|
|
139
|
+
const testConnectionString = buildTestConnectionString(); // Utilise la définition interne
|
|
140
|
+
const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
|
|
198
141
|
connection = await odbcModule.connect(connectionOptions);
|
|
199
142
|
res.sendStatus(200);
|
|
200
143
|
} catch (err) {
|
|
201
|
-
console.error("[ODBC Test] Connection failed:", err);
|
|
202
144
|
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
203
145
|
} finally {
|
|
204
|
-
if (connection)
|
|
205
|
-
await connection.close(); // Fermeture simple, pas besoin de timeout ici car c'est une op rapide.
|
|
206
|
-
}
|
|
146
|
+
if (connection) await connection.close();
|
|
207
147
|
}
|
|
208
148
|
});
|
|
209
149
|
|
|
@@ -215,88 +155,69 @@ module.exports = function (RED) {
|
|
|
215
155
|
this.name = this.config.name;
|
|
216
156
|
this.isAwaitingRetry = false;
|
|
217
157
|
this.retryTimer = null;
|
|
158
|
+
this.cursorCloseOperationTimeout = 5000;
|
|
159
|
+
this.currentQueryForErrorContext = null; // Pour stocker la requête lors du traitement
|
|
160
|
+
this.currentParamsForErrorContext = null; // Pour stocker les paramètres lors du traitement
|
|
218
161
|
|
|
219
|
-
// NOUVEAU: Timeout fixe pour les opérations de fermeture de curseur (en ms)
|
|
220
|
-
this.cursorCloseOperationTimeout = 5000; // 5 secondes
|
|
221
162
|
|
|
222
|
-
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
163
|
+
this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
|
|
164
|
+
// Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
|
|
165
|
+
const q = query || this.currentQueryForErrorContext;
|
|
166
|
+
const p = params || this.currentParamsForErrorContext;
|
|
167
|
+
const queryContext = (() => {
|
|
168
|
+
let s = "";
|
|
169
|
+
if (q || p) {
|
|
170
|
+
s += " {";
|
|
171
|
+
if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
|
|
172
|
+
if (p) s += `, "params": '${JSON.stringify(p)}'`;
|
|
173
|
+
s += "}";
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
return "";
|
|
177
|
+
})();
|
|
178
|
+
let finalError;
|
|
179
|
+
if (typeof error === "object" && error !== null && error.message) { finalError = error; }
|
|
180
|
+
else if (typeof error === "string") { finalError = new Error(error); }
|
|
181
|
+
else { finalError = new Error(defaultMessage); }
|
|
182
|
+
finalError.message = `${finalError.message}${queryContext}`;
|
|
183
|
+
if (q) finalError.query = String(q).substring(0,200);
|
|
184
|
+
if (p) finalError.params = p;
|
|
185
|
+
return finalError;
|
|
186
|
+
};
|
|
187
|
+
|
|
223
188
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
189
|
+
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => { /* ... (inchangé) ... */ };
|
|
190
|
+
|
|
191
|
+
// NOUVELLE fonction utilitaire
|
|
192
|
+
async function testBasicConnectivity(connection, nodeInstance) {
|
|
193
|
+
if (!connection || typeof connection.query !== 'function') {
|
|
194
|
+
nodeInstance.warn("Test de connectivité basique : connexion invalide fournie.");
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
231
197
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const rows = await cursor.fetch();
|
|
242
|
-
if (!rows || rows.length === 0) { break; }
|
|
243
|
-
|
|
244
|
-
for (const row of rows) {
|
|
245
|
-
rowCount++;
|
|
246
|
-
chunk.push(row);
|
|
247
|
-
if (chunk.length >= chunkSize) {
|
|
248
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
249
|
-
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
250
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
251
|
-
send(newMsg);
|
|
252
|
-
chunk = [];
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (chunk.length > 0) {
|
|
258
|
-
const newMsg = RED.util.cloneMessage(msg);
|
|
259
|
-
objPath.set(newMsg, this.config.outputObj, chunk);
|
|
260
|
-
newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
|
|
261
|
-
send(newMsg);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const finalMsg = RED.util.cloneMessage(msg);
|
|
265
|
-
objPath.set(finalMsg, this.config.outputObj, []);
|
|
266
|
-
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
267
|
-
send(finalMsg);
|
|
268
|
-
|
|
269
|
-
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
270
|
-
|
|
271
|
-
} finally {
|
|
272
|
-
if (cursor) {
|
|
273
|
-
try {
|
|
274
|
-
// NOUVEAU: Timeout pour la fermeture du curseur
|
|
275
|
-
await Promise.race([
|
|
276
|
-
cursor.close(),
|
|
277
|
-
new Promise((_, reject) =>
|
|
278
|
-
setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout)
|
|
279
|
-
)
|
|
280
|
-
]);
|
|
281
|
-
} catch (cursorCloseError) {
|
|
282
|
-
this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
198
|
+
const originalTimeout = connection.queryTimeout;
|
|
199
|
+
connection.queryTimeout = 5; // Court timeout pour un SELECT 1
|
|
200
|
+
await connection.query("SELECT 1"); // Ou équivalent SGBD
|
|
201
|
+
connection.queryTimeout = originalTimeout;
|
|
202
|
+
nodeInstance.log("Test de connectivité basique (SELECT 1) : Réussi.");
|
|
203
|
+
return true;
|
|
204
|
+
} catch (testError) {
|
|
205
|
+
nodeInstance.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
|
|
206
|
+
return false;
|
|
285
207
|
}
|
|
286
|
-
}
|
|
208
|
+
}
|
|
287
209
|
|
|
288
|
-
// MODIFIÉ: Ajout de la définition du queryTimeout sur la connexion
|
|
289
210
|
this.on("input", async (msg, send, done) => {
|
|
211
|
+
this.currentQueryForErrorContext = null;
|
|
212
|
+
this.currentParamsForErrorContext = null;
|
|
213
|
+
|
|
290
214
|
if (this.isAwaitingRetry) {
|
|
291
215
|
if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
|
|
292
216
|
this.log("New message received, overriding retry timer and attempting query now.");
|
|
293
|
-
clearTimeout(this.retryTimer);
|
|
294
|
-
this.retryTimer = null;
|
|
295
|
-
this.isAwaitingRetry = false;
|
|
217
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
296
218
|
} else {
|
|
297
|
-
this.warn("Node is in a retry-wait state. New message ignored
|
|
298
|
-
if (done) done();
|
|
299
|
-
return;
|
|
219
|
+
this.warn("Node is in a retry-wait state. New message ignored.");
|
|
220
|
+
if (done) done(); return;
|
|
300
221
|
}
|
|
301
222
|
}
|
|
302
223
|
this.isAwaitingRetry = false;
|
|
@@ -307,30 +228,33 @@ module.exports = function (RED) {
|
|
|
307
228
|
return done(new Error("ODBC Config node not properly configured."));
|
|
308
229
|
}
|
|
309
230
|
|
|
310
|
-
const
|
|
311
|
-
this.config.outputObj = this.config.outputObj || "payload";
|
|
231
|
+
const getRenderedQueryAndParams = async () => {
|
|
312
232
|
const querySourceType = this.config.querySourceType || 'msg';
|
|
313
233
|
const querySource = this.config.querySource || 'query';
|
|
314
234
|
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
315
235
|
const paramsSource = this.config.paramsSource || 'parameters';
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!
|
|
321
|
-
|
|
236
|
+
|
|
237
|
+
this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
238
|
+
this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
239
|
+
|
|
240
|
+
if (!this.currentQueryForErrorContext) throw new Error("No query to execute");
|
|
241
|
+
|
|
242
|
+
let finalQuery = this.currentQueryForErrorContext;
|
|
243
|
+
const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
|
|
244
|
+
if (!isPreparedStatement && finalQuery) {
|
|
245
|
+
finalQuery = mustache.render(finalQuery, msg);
|
|
322
246
|
}
|
|
247
|
+
return { query: finalQuery, params: this.currentParamsForErrorContext };
|
|
248
|
+
};
|
|
323
249
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
this.warn(`Could not set queryTimeout on connection: ${e.message}`);
|
|
331
|
-
}
|
|
250
|
+
const executeUserQuery = async (connection, query, params) => {
|
|
251
|
+
// Appliquer le queryTimeout configuré
|
|
252
|
+
const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
253
|
+
if (configuredTimeout > 0) {
|
|
254
|
+
try { connection.queryTimeout = configuredTimeout; }
|
|
255
|
+
catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
|
|
332
256
|
} else {
|
|
333
|
-
connection.queryTimeout = 0; //
|
|
257
|
+
connection.queryTimeout = 0; // Infini ou défaut driver
|
|
334
258
|
}
|
|
335
259
|
|
|
336
260
|
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
@@ -343,67 +267,107 @@ module.exports = function (RED) {
|
|
|
343
267
|
}
|
|
344
268
|
};
|
|
345
269
|
|
|
346
|
-
let
|
|
347
|
-
let
|
|
270
|
+
let activeConnection = null; // Pour gérer la connexion active (pool ou fraîche)
|
|
271
|
+
let shouldProceedToTimedRetry = false;
|
|
272
|
+
let errorForTimedRetry = null;
|
|
348
273
|
|
|
349
|
-
try {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
274
|
+
try { // Tentative Principale (avec connexion du pool)
|
|
275
|
+
const { query, params } = await getRenderedQueryAndParams();
|
|
276
|
+
|
|
277
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
|
|
278
|
+
activeConnection = await this.poolNode.connect();
|
|
279
|
+
await executeUserQuery(activeConnection, query, params);
|
|
280
|
+
|
|
281
|
+
if (activeConnection) { await activeConnection.close(); activeConnection = null; }
|
|
353
282
|
return done();
|
|
354
|
-
|
|
355
|
-
|
|
283
|
+
|
|
284
|
+
} catch (initialError) {
|
|
285
|
+
this.warn(`Initial attempt failed: ${initialError.message}`);
|
|
286
|
+
if (activeConnection) { // Si la connexion a été obtenue mais que executeUserQuery a échoué
|
|
287
|
+
const connStillGood = await testBasicConnectivity(activeConnection, this);
|
|
288
|
+
try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing pool conn after initial error: "+e.message);}
|
|
289
|
+
|
|
290
|
+
if (connStillGood) { // La connexion est bonne, l'erreur vient de la requête utilisateur
|
|
291
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error" });
|
|
292
|
+
return done(this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error"));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Si on arrive ici, la connexion poolée a eu un problème (soit pour se connecter, soit SELECT 1 a échoué)
|
|
296
|
+
|
|
356
297
|
if (this.poolNode.config.retryFreshConnection) {
|
|
357
298
|
this.warn("Attempting retry with a fresh connection.");
|
|
358
299
|
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
359
|
-
let freshConnection;
|
|
360
300
|
try {
|
|
361
301
|
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
362
|
-
|
|
363
|
-
this.log("Fresh connection established
|
|
364
|
-
|
|
302
|
+
activeConnection = await odbcModule.connect(freshConnectConfig);
|
|
303
|
+
this.log("Fresh connection established.");
|
|
304
|
+
|
|
305
|
+
const freshConnGood = await testBasicConnectivity(activeConnection, this);
|
|
306
|
+
if (!freshConnGood) {
|
|
307
|
+
// Erreur de connectivité même sur une connexion fraîche
|
|
308
|
+
errorForTimedRetry = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Fresh Connection Test Failed");
|
|
309
|
+
shouldProceedToTimedRetry = true;
|
|
310
|
+
throw errorForTimedRetry; // Va au catch externe de ce bloc try-fresh
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// La connexion fraîche est bonne, on retente la requête utilisateur originale
|
|
314
|
+
const { query, params } = await getRenderedQueryAndParams(); // Re-préparer au cas où
|
|
315
|
+
await executeUserQuery(activeConnection, query, params);
|
|
316
|
+
|
|
365
317
|
this.log("Query successful with fresh connection. Resetting pool.");
|
|
366
318
|
await this.poolNode.resetPool();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
319
|
+
if (activeConnection) { await activeConnection.close(); activeConnection = null; }
|
|
320
|
+
return done(); // Succès !
|
|
321
|
+
|
|
322
|
+
} catch (freshErrorOrConnectivityFail) {
|
|
323
|
+
// Soit odbcModule.connect a échoué, soit SELECT 1 a échoué (et errorForTimedRetry est déjà setté),
|
|
324
|
+
// soit executeUserQuery sur la connexion fraîche a échoué.
|
|
325
|
+
if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
|
|
326
|
+
|
|
327
|
+
if (shouldProceedToTimedRetry) { // Signifie que SELECT 1 sur la connexion fraîche a échoué
|
|
328
|
+
// errorForTimedRetry est déjà setté
|
|
329
|
+
} else {
|
|
330
|
+
// SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
|
|
331
|
+
this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
|
|
332
|
+
return done(this.enhanceError(freshErrorOrConnectivityFail, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error (on fresh connection)"));
|
|
333
|
+
}
|
|
372
334
|
}
|
|
373
|
-
} else {
|
|
374
|
-
|
|
335
|
+
} else { // Pas de retryFreshConnection configuré, l'erreur initiale était donc un problème de connexion.
|
|
336
|
+
errorForTimedRetry = this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Connection Error");
|
|
337
|
+
shouldProceedToTimedRetry = true;
|
|
375
338
|
}
|
|
376
|
-
} finally {
|
|
377
|
-
if (connectionFromPool) await connectionFromPool.close();
|
|
378
339
|
}
|
|
379
|
-
|
|
380
|
-
|
|
340
|
+
|
|
341
|
+
// Logique de Retry Temporisé
|
|
342
|
+
if (shouldProceedToTimedRetry && errorForTimedRetry) {
|
|
381
343
|
const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
|
|
382
344
|
if (retryDelaySeconds > 0) {
|
|
383
|
-
this.warn(`
|
|
345
|
+
this.warn(`Connection issue suspected. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorForTimedRetry.message}`);
|
|
384
346
|
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
|
|
385
347
|
this.isAwaitingRetry = true;
|
|
386
348
|
this.retryTimer = setTimeout(() => {
|
|
387
|
-
this.isAwaitingRetry = false;
|
|
388
|
-
this.
|
|
389
|
-
this.log(`Retry timer expired for message. Re-emitting for node ${this.id || this.name}.`);
|
|
349
|
+
this.isAwaitingRetry = false; this.retryTimer = null;
|
|
350
|
+
this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
|
|
390
351
|
this.receive(msg);
|
|
391
352
|
}, retryDelaySeconds * 1000);
|
|
392
|
-
|
|
393
|
-
} else {
|
|
394
|
-
this.status({ fill: "red", shape: "ring", text: "
|
|
395
|
-
|
|
353
|
+
return done(); // Termine l'invocation actuelle du message
|
|
354
|
+
} else { // Pas de délai de retry, ou délai à 0
|
|
355
|
+
this.status({ fill: "red", shape: "ring", text: "Connection Error" });
|
|
356
|
+
return done(errorForTimedRetry);
|
|
396
357
|
}
|
|
358
|
+
} else if (errorForTimedRetry) { // Une erreur SQL a été identifiée et ne doit pas déclencher de retry de connexion
|
|
359
|
+
this.status({ fill: "red", shape: "ring", text: "Error (No Retry)" });
|
|
360
|
+
return done(errorForTimedRetry); // Devrait déjà avoir été fait
|
|
397
361
|
} else {
|
|
398
|
-
|
|
362
|
+
// Normalement, on ne devrait pas arriver ici si done() a été appelé après un succès.
|
|
363
|
+
this.log("[ODBC Node] DEBUG: Reached end of on('input') without error or prior done(). Calling done().");
|
|
364
|
+
return done();
|
|
399
365
|
}
|
|
400
366
|
});
|
|
401
367
|
|
|
402
368
|
this.on("close", (done) => {
|
|
403
369
|
if (this.retryTimer) {
|
|
404
|
-
clearTimeout(this.retryTimer);
|
|
405
|
-
this.retryTimer = null;
|
|
406
|
-
this.isAwaitingRetry = false;
|
|
370
|
+
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
407
371
|
this.log("Cleared pending retry timer on node close/redeploy.");
|
|
408
372
|
}
|
|
409
373
|
this.status({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A powerful Node-RED node to connect to any ODBC data source, with connection pooling, advanced retry logic, and result streaming.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|