Forum: PC-Programmierung Node Fehler behandeln wenn Datenbank timeout hat


von Aua (Gast)


Lesenswert?

Hi Leute,

ich bastele ja nun schon einige Zeit mit Docker und node.js. Als Basis 
benutze ich dieses Tutorial und alles funktioniert inzwischen prima. Ich 
habe auch die Datenbank so eingebunden wie hier beschrieben.

https://bezkoder.com/node-js-rest-api-express-mysql/

Gestern Abend habe ich dann gesehen, dass es plötzlich (ohne dass ich 
auf die Datenbank zugegriffen hätte) diesen Fehler gab (siehe unten). 
Aber da der ja nicht von meinem code oder meinen Aktivitäten ausging - 
wie kann ich den abfangen / behandeln? c_db ist der Datenbank-Container 
und c_node der für node
1
c_db      | 2021-07-26 23:04:43 3 [Warning] Aborted connection 3 to db: 'db_XXX' user: 'u_XXX' host: '172.18.0.3' (Got timeout reading communication packets)
2
c_node    | node:events:371
3
c_node    |       throw er; // Unhandled 'error' event
4
c_node    |       ^
5
c_node    | 
6
c_node    | Error: read ECONNRESET
7
c_node    |     at TCP.onStreamRead (node:internal/stream_base_commons:211:20)
8
c_node    | Emitted 'error' event on Connection instance at:
9
c_node    |     at Connection._handleProtocolError (/home/node/app/node_modules/mysql/lib/Connection.js:423:8)
10
c_node    |     at Protocol.emit (node:events:394:28)
11
c_node    |     at Protocol._delegateError (/home/node/app/node_modules/mysql/lib/protocol/Protocol.js:398:10)
12
c_node    |     at Protocol.handleNetworkError (/home/node/app/node_modules/mysql/lib/protocol/Protocol.js:371:10)
13
c_node    |     at Connection._handleNetworkError (/home/node/app/node_modules/mysql/lib/Connection.js:418:18)
14
c_node    |     at Socket.emit (node:events:394:28)
15
c_node    |     at emitErrorNT (node:internal/streams/destroy:157:8)
16
c_node    |     at emitErrorCloseNT (node:internal/streams/destroy:122:3)
17
c_node    |     at processTicksAndRejections (node:internal/process/task_queues:83:21) {
18
c_node    |   errno: -104,
19
c_node    |   code: 'ECONNRESET',
20
c_node    |   syscall: 'read',
21
c_node    |   fatal: true
22
c_node    | }
23
c_node exited with code 1

von Εrnst B. (ernst)


Lesenswert?

1
try { ... } catch (...) { ... }
https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/try...catch

oder, speziell auf das MySQL-Modul bezogen:
1
myConnection.on('error',(e) => { 
2
   console.log("MySQL-Fehler wurde nicht abgefangen",e); 
3
   process.exit(1); 
4
});

: Bearbeitet durch User
von Aua (Gast)


Lesenswert?

Try-catch kenne ich und habe es auch schon benutzt. Das Ding ist hier: 
Ich wusste nicht, wo ich es hätte hinschreiben sollen - um die gesamte 
server.js? Ich hatte inzwischen erstmal so probiert und den Fehler 
gefangen (Deine Vorgabe ist spezifischer)
1
process.on('uncaughtException', function(err) {
2
  console.error(err.stack);
3
  console.log('catched - NODE can run on.');
4
});

So wie ich das sehe, wird das connection-"Objekt?" in der db.js erzeugt 
und dann nur in der "customer.model.js" benutzt, also die server.js weiß 
davon gar nichts?

Jedenfalls - was ich erreichen will: Derzeit (ohne die o.g. Änderung) 
stürzt das Programm ab, sobald die Datenbank nur kurz nicht zur 
Verfügung steht. Ich würde in dem Fall gerne nach einer Minute 
versuchen, neu zu verbinden.

Für mein Verständnis müsste ich dann in der o.g. Funktion ein callback 
in einer Minute vereinbaren, und da neu verbinden. Aber wenn der 
Programmteil in der server.js steht, dann weiß die ja gar nichts von der 
Objektinstanz innerhalb des Moduls..?

PS: Ich bin ja auf dem langen Weg vom Abschreiben zum Verstehen und Du 
hast mit schon sehr geholfen, danke!

von 🐧 DPA 🐧 (Gast)


Lesenswert?

https://www.npmjs.com/package/mysql#error-handling
> Last but not least: If a fatal errors occurs and there are no pending
> callbacks, or a normal error occurs which has no callback belonging to it,
> the error is emitted as an 'error' event on the connection object. This is
> demonstrated in the example below:
1
#!/usr/bin/env node
2
connection.on('error', function(err) {
3
  console.log(err.code); // 'ER_BAD_DB_ERROR'
4
});

Oft ist es aber von Vorteil, einen Connection Pool zu haben. Da hat man 
dann immer ein paar Verbindungen auf Vorrat. Man holt sich daraus dann 
immer erst eine Verbindung, wenn man gerade eine braucht. Wenn dann da 
zwischendrin mal eine Abbricht, ist das oft nicht so schlimm.
https://www.npmjs.com/package/mysql#pooling-connections oder 
https://www.npmjs.com/package/generic-pool

Man sollte dann aber auch noch mit Transactions arbeiten, damit man bei 
Komplexeren Sachen nichts halbfertiges hat. Fast alles in Transactions 
wird bei Fehlern und Verbindungsabbrüchen rückgängig gemacht / bzw. erst 
beim Commit übernommen. Wobei, bei sowas simplem wie in deinem 
Beispiellink (nur ein Query pro Funktion / Rest call) kann man sich das 
auch sparen. Das Beispiel ist aber auch sehr schlecht geeignet, da 
nachträglich noch Transaktionen nachzurüsten, weil man da die DB 
Verbindung mit der Transaktion nicht mitgeben kann.

Das holen von Verbindungen, das Verwalten von Transactions, usw. packe 
ich mir gerne in Hilfsfunktionen, damit ich aquire, release, commit, 
rollback, etc. nicht vergesse. Ich mache dann alle DB Funktionen gerne 
async. z.B.
1
// (alles ungetestet)
2
3
const pf = (t,f)=>promisify((...x)=>t[f](...x)); // Like promisify, but preserve this
4
5
async function connection_helper(c, callback){
6
  if(c) return c;
7
  const connection = await pf(pool,'getConnection');
8
  try {
9
    await callback(connection);
10
  } finally {
11
    connection.release();
12
  }
13
}
14
15
const transaction_private = Symbol('transaction');
16
async function transaction_helper(connection, callback){
17
  if(!connection[transaction_private])
18
    connection[transaction_private] = {sp: null, id: 0};
19
  const id = connection[transaction_private].id;
20
  connection[transaction_private].id = connection[transaction_private].id + 1 |0;
21
  const current_savepoint = {id};
22
  const old_savepoint = connection[transaction_private].sp;
23
  connection[transaction_private].sp = current_savepoint;
24
  // Transaction oder Savepoint starten
25
  if(old_savepoint){
26
    await pf(connection,'query')(`SAVEPOINT sp_${current_savepoint.id}`);
27
  }else{
28
    await pf(connection,'beginTransaction')();
29
  }
30
  wrong_savepoint: {
31
    try {
32
      const result = await callback(connection);
33
      if(connection[transaction_private].sp !== current_savepoint)
34
        break wrong_savepoint;
35
      try {
36
        // Release savepoint or commit if none
37
        if(old_savepoint){
38
          await pf(connection,'query')(`RELEASE SAVEPOINT sp_${current_savepoint.id}`);
39
        }else{
40
          await pf(connection,'commit')();
41
        }
42
      } finally {
43
        if(connection[transaction_private].sp !== current_savepoint)
44
          break wrong_savepoint;
45
      }
46
      return result;
47
    } catch(e) {
48
      if(connection[transaction_private].sp !== current_savepoint)
49
        break wrong_savepoint;
50
      try {
51
        if(old_savepoint){
52
          await pf(connection,'query')(`ROLLBACK TO SAVEPOINT sp_${current_savepoint.id}`);
53
        }else{
54
          await pf(connection,'rollback')();
55
        }
56
      } finally {
57
        if(connection[transaction_private].sp !== current_savepoint)
58
          break wrong_savepoint;
59
        throw e;
60
      }
61
    } finally {
62
      if(connection[transaction_private].sp !== current_savepoint)
63
        break wrong_savepoint;
64
      connection[transaction_private].sp = old_savepoint;
65
    }
66
    return;
67
  }
68
  try {
69
    connection[transaction_private].sp = null;
70
    connection.destroy(); // Something went very wrong. Terminate connection, that should roll back any pending transactions, and makes sure nothing else is done on this connection
71
  } finally {
72
    throw new Error("Unexpected savepoint. Make sure not to call these functions in paralel, and don't forget to use await everywhere!");
73
  }
74
}

Und dann:
1
async function do_something(c, bla){
2
  return connection_helper(c, async c=>{
3
    let result = await pf(db,'query')("SELECT bla FROM bla WHERE x=?", [bla]);
4
    return result[0].bla;
5
  });
6
}
7
8
async function do_something_2(c, bla){
9
  return connection_helper(c, c=>transaction_helper(c, async()=>{
10
    await pf(db,'query')("UPDATE bla2 SET x=?", [bla]);
11
    await pf(db,'query')("UPDATE bla3 SET x=?", [bla]);
12
  }));
13
}
14
15
async function do_something_complex(c){
16
  return connection_helper(c, c=>transaction_helper(c, async()=>{
17
    let result = await do_something(db, 123);
18
    await do_something_2(db, result);
19
  }));
20
}

von 🐧 DPA 🐧 (Gast)


Lesenswert?

Edit: Transaction Deadlocks sind im Code oben noch nicht behandelt. 
Sowas in der Art:
1
  async function db_deadlock_retry(func, count=20){
2
    let i=0;
3
    for(let i=0; !count || i<count; i++){
4
      try {
5
        return await func();
6
      } catch(error) {
7
        if(error.code == 'ER_LOCK_DEADLOCK'){
8
          console.info("Got "+error.code+"...");
9
          continue;
10
        }
11
        throw error;
12
      }
13
    }
14
  }

Sollte man aber nur in der toplevel transaction machen, nicht für 
einzelne savepoints.

von Aua (Gast)


Lesenswert?

zu "connection pool" hatte ich etwas gelesen, das hilft mir aber nicht, 
wenn die Datenbank ein paar Minuten "weg" ist (z.B. Server wird neu 
gestartet) und dann wiederkommt.

von 🐧 DPA 🐧 (Gast)


Lesenswert?

Man muss sich halt die Verbindung erst dann holen, wenn man sie auch 
braucht, und danach wieder freigeben oder schliessen. Wenn die DB weg 
ist, man was machen will, und es deshalb Fehler gibt, muss man es dann 
halt später nochmal versuchen, wenn die DB wieder da ist.

von Aua (Gast)


Lesenswert?

Ich habe jetzt mal ein Mini-Testprogramm geschrieben, db.js ist gebause 
wie im Tutorial oben:

[code]
const express = require("express");
const app = express();
app.use(express.json());
app.use(express.urlencoded());

// catch database connection errors. ToDo: do more specific on 
ECONNRESET
process.on('uncaughtException', function(err) {
  console.error(err.stack);
  console.log('catched - NODE can run on.');
})

// db connection
var sql = require("./models/db.js");

app.get("/", (req, res) => {
  sql.query("SELECT * FROM customers", function(err, result) {
    if (err) {res.json({err})} else {res.json({ result })};
  });
});

// set port, listen for requests
app.listen(3133, () => {
  console.log("Server is running on port 3133.");
});
[code]

Das funktioniert, und wenn ich die Datenbank abschalte, gibt es sofort 
einen Fehler, den ich ja abfange, also der "server" läuft weiter. Aber 
was muss ich wie mit der Connection tun, damit es wieder funktioniert, 
sobald die DB wieder da ist?

Momentan erhalte ich da den Fehler: PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR

habe schon versucht vor sql.query zu schreiben
sql.destroy();
sql.connect();

aber das hat nicht funktioniert..

von Aua (Gast)


Lesenswert?

Was ich inzwischen noch gelesen habe: Man muss nach einem solchen Fehler 
ein neues Connection-Objekt benutzen - aber wie mache ich das (wie 
kriege ich es ganz tot), bzw. wie stelle ich sicher, dass er nicht immer 
wieder durch die Modul-Verwendung das alte benutzt?

von Εrnst B. (ernst)


Lesenswert?

Aua schrieb:
> Was ich inzwischen noch gelesen habe: Man muss nach einem solchen Fehler
> ein neues Connection-Objekt benutzen

das "Wie" stand weiter oben:

🐧 DPA 🐧 schrieb:
> Oft ist es aber von Vorteil, einen Connection Pool zu haben.

da kümmert sich dann das "Pool-Verwaltungs-Objekt" darum, dass du immer 
eine funktionierende Verbindung bekommst.

https://www.npmjs.com/package/mysql#pooling-connections

von 🐧 DPA 🐧 (Gast)


Lesenswert?

Indem du bei db.js keine bereits geöffnete Verbindung exportierst, 
sondern stattdessen Funktionen, mit denen du dir eine Verbindung holst, 
und/oder sie wieder frei gibst.

Eine ganz Primitive Methode (mit ES5 old style callbacks stat 
async/await):
1
#!node
2
function aquireDBConnection(callback){
3
  const connection = mysql.createConnection({
4
    host: dbConfig.HOST,
5
    user: dbConfig.USER,
6
    password: dbConfig.PASSWORD,
7
    database: dbConfig.DB
8
  });
9
10
  // open the MySQL connection
11
  connection.connect(error => {
12
    if (error) return callback(error);
13
    console.log("Successfully connected to the database.");
14
    callback(null, connection);
15
  });
16
}
17
18
function releaseDBConnection(connection){
19
  connection.end();
20
}
21
22
module.exports = {aquireDBConnection, releaseDBConnection};
(Da nimmt man statdessen aber besser einen connection pool, der auch 
gleich solche Methoden schon anbietet. Das ist nur illustratorisch.).

Und dann:
1
#!node
2
var {aquireDBConnection, releaseDBConnection} = require("./models/db.js");
3
4
app.get("/", (req, res) => {
5
  aquireDBConnection(function(error, sql){ // get a connection before using it
6
    if (error) throw error;
7
    sql.query("SELECT * FROM customers", function(err, result) {
8
      releaseDBConnection(sql); // Connection no longer needed, release it
9
      if (err) {res.json({err})} else {res.json({ result })};
10
    });
11
  });
12
});

In meinem ersten post ist viel async / await zucker, um das Freigeben 
von Verbindungen und error handling wegzuabstrahieren.

von Aua (Gast)


Lesenswert?

Der connection pool hat's gebracht, und ich musste nur wenig ändern. Die 
db.js aus dem Beispiel/tutorial oben sieht jetzt so aus / alles schick:
1
const mysql = require("mysql");
2
const dbConfig = require("../config/db.config.js");
3
4
// Create a connection to the database
5
const connection = mysql.createPool({
6
  //ToDo:increase later
7
  connectionLimit: 2,
8
  host: dbConfig.HOST,
9
  user: dbConfig.USER,
10
  password: dbConfig.PASSWORD,
11
  database: dbConfig.DB
12
});
13
14
/*
15
// open the MySQL connection
16
connection.connect(error => {
17
  if (error) throw error;
18
  console.log("Successfully connected to the database.");
19
});
20
*/
21
22
module.exports = connection;

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.