ChuckBell / MySQL_Connector_Arduino

Database connector library for using MySQL with your Arduino projects.
331 stars 132 forks source link

DB Write Locks Up Arduino Processing After Random Time #93

Closed madmacks59 closed 2 years ago

madmacks59 commented 5 years ago

I have a project that involves a Mega2560, Ethernet 2 Shield, Freetronics 1602 LCD and a ultrasonic sensor. The project is reading the level of my well tank and then updating a MySQL DB on a periodic basis. Everything has been working so far but a couple of days ago I started QA testing the project by leaving it running 24x7 in my off grid cabin prior to installing in the water tank. I started getting random hang, sometime after only 10-15 minutes, sometimes after hours or process. For testing I have the sketch reading and updating every minute; but only update to the DB if the sensor readings have changed since the last DB write. I had issues with the sensor being unstable and had to update the code to use a library called NewPing, that seemed to resolve the out of bounds sensor readings. Now I'm seeing my DB writes hang the sketch, randomly. I've started monitoring my MySQL (10.1.37-MariaDB-0+deb9u1) logs and see the connections and inserts running. I don't see any fails or the DB dropping the user, so as far as I can tell I'm still maintaining a DB connection. I've also monitored my router to make sure the Arduino is connected (as well as the MySQL server), and all seems OK. Based on adding a slew of more LCD display I see the process hang at the "Writing To DB" message which is directly before my "cur_mem->execute(query);" statement. Right now I'm stuck! Below is the sketch if you have time to take a look. If you have any questions or comments please let me know. And if there's a better way to provide a readable version of the sketch let me know. The below looks like a mess without the formatting...ugh...

/*****************************************************************************************************************************
     Well Tank Water Level Sketch

       Author: Gary, 2019-03-08
        Using: Arduino Mega
               Freetronics LCD with buttons (ignore Buttons)
               Arduino Ethernet Shield 2
               Robot Shop Weatherproof Ultrasonic Sensor w/ Separate Probe
     Function: Establishes an ethernet connection and then attaches to MySQL. Then the
               ultrasonic sensor is read, calcs are done and the well_tank DB is updated.
               The sensor should never read and return zero as there should always be
               an airgap at the top of the tank. The sensor is waterproof but let's not
               test that in practice...
 ******************************************************************************************************************************/
/*Serial, Ethernet, MySQL, NewPing Includes */
#include <SPI.h>
#include <Ethernet.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>
/* LCD Includes */
#include <Wire.h>
#include <LiquidCrystal.h>
#include <NewPing.h>
/*LCD Defines */
#define BUTTON_ADC_PIN           A0                         // A0 is the button LCD input
#define LCD_BACKLIGHT_PIN         3                         // D3 controls LCD backlight
/*Init the LCD library with the LCD pins to be used */
LiquidCrystal lcd( 8, 9, A4, 5, 6, 7 );                     //Pins for Freetronics 16x2 LCD shield. ( RS, E, D4, D5, D6, D7 )
/* Serial, Ethernet, and MySQL set up code */
/*
    Note, if the MAC changes then DHCP reservation IP address will not work
*/
byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xFE };   // Assign psuedo MAC address to Ethernet Shield
// Create needed DB connection objects and parms
IPAddress server_addr(192, 168, 1, 100);                    // IP of the Raspberry Pi MySQL Server
char user[] = "arduino";                                    // MySQL user login username
char password[] = "waterMan59";                             // MySQL user login password
//Establish Ethernet and MySQL objects
EthernetClient client;
MySQL_Connection conn((Client *)&client);
// Set Upt the Sensor
#define TRIGGER_PIN  36                            // Set to the Arduino Sensor Trigger Pin
#define ECHO_PIN     38                            // Set to the Arduino Sensor Echo Pin
#define MAX_DISTANCE 183                           // Calc using (maxheight + airspace) * 2.54 rounded for CM
//Initialize working variables
int capacity = 2600;                               // Set to max capacity of the tank in gallons
int maxheight = 60;                                // Set to the max height in inches of the wate level of the tank (Max 96")
int fudgefactor = 0;                               // Use for calibration of device, in inches + or - depending or readings
int airspace = 12;                                 // Set to the airspace at the top of the tank when full
int waterheight;                                   // Should be the depth of the water in the tank
float percent;                                     // Used to compute the gallons in the tank
int gallons;                                       // Should be the gallons of water in the tank
int priorgallons = 0;                              // Holds prior reading to help cut down on DB rows
int pingcount = 7;                                 // Number of times a reading should be taken before reporting a distnace
int errorcount = 0;                                // Sensor error count since the last reboot
char blank16[] = "                ";               // Used to blank left 16 characters of a display line
char blank13[] = "             ";                  // Used to blank left 13 characters of a display line
/* Create a sonar ping object */
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE);

/*
    Wait time is implemented in minutes using an integer variable. Therefore it will get funky if you want to read the
    sensor less than every approximately 9 hours (9 hrs is 32,400 seconds and signed integers only hold up to 32,767 seconds).
    You should probably read the water depth more requently that every 9 hours or the tank will likely flood anyway...
    So, set to the wait time between readings in minutes, must be at least 1 and please no more than 546!
*/
int waittime = 1;                                  // Set in whole minutes

int lanConnect() {                                 // My Ethernet Connect Routine
  if (Ethernet.begin(mac_addr)) {
    lcd.setCursor( 0, 0 );
    lcd.print("Ethernet ADDR:  ");
    lcd.setCursor( 0, 1 );
    lcd.print(blank16);
    lcd.setCursor( 0, 1 );
    lcd.print(Ethernet.localIP());
    delay(2000);
    return 1;
  }
  else {
    lcd.setCursor( 0, 0 );
    lcd.print("Ethernet Failed!");
    lcd.setCursor( 0, 1 );
    lcd.print(blank16);
    delay(5000);
    return 0;
  }
}

/*Set Up Loop Runs Once */
void setup() {
  Serial.begin(9600);
  pinMode(A4, OUTPUT);                             // Set up A4 as digital for LCD Shield
  digitalWrite(A4, HIGH);                          // Set up A4 as digital for LCD Shield
  pinMode( BUTTON_ADC_PIN, INPUT );                // ensure A0 is an input
  digitalWrite( BUTTON_ADC_PIN, LOW );             // ensure pullup is off on A0
  digitalWrite( LCD_BACKLIGHT_PIN, HIGH );         // backlight control pin D3 is high (on)
  pinMode( LCD_BACKLIGHT_PIN, OUTPUT );            // D3 is an output
  lcd.begin( 16, 2 );                              // Create LCD object and define the number of columns and rows
  lcd.setCursor( 0, 0 );                           // Char1, Row1
  lcd.print("  Semi-Amazing  ");                   // Print initial message to the LCD
  lcd.setCursor( 0, 1 );                           // Char1, Row2
  lcd.print("Well Tank Sensor");                   // Print initial message to the LCD
  delay(2000);
  /* Ethernet and MySQL */
  if (lanConnect()) {
    if (conn.connect(server_addr, 3306, user, password)) {
      lcd.setCursor( 0, 0 );
      lcd.print("MySQL Connected!");
      lcd.setCursor( 0, 1 );
      lcd.print(blank16);
    }
    else {
      lcd.setCursor( 0, 0 );
      lcd.print("MySQL Failed!   ");
      lcd.setCursor( 0, 1 );
      lcd.print(blank16);
    }
  }
  delay(5000);
}

void loop() {
  // Set up and send the initial sensor reset echos
  /*
      Note: for the LCD display the last char of the second line is used to report that a sensor
            reading error occured since the last cold reboot. So, the 15 and 16th characters are not
            reset in the LCD displays inside the main loop and if a digit shows in the last two
            characters it's the error count (tuncated to 2 digit)...
  */
  lcd.setCursor( 0, 0 );
  lcd.print("Reading Sensor  ");
  lcd.setCursor( 0, 1 );
  lcd.print(blank13);
  delay(2000);
  int distance = sonar.ping_in();
  lcd.setCursor( 0, 0 );
  lcd.print("Sensor Rd Done  ");
  lcd.setCursor( 0, 1 );
  lcd.print(blank13);
  delay(2000);
  Serial.print("Single/Median Ping Reading: ");
  Serial.print(distance);
  distance = sonar.convert_in(sonar.ping_median(pingcount));
  lcd.setCursor( 0, 0 );
  lcd.print("Median Cnvt Done");
  lcd.setCursor( 0, 1 );
  lcd.print(blank13);
  Serial.print("/");
  Serial.println(distance);
  delay(2000);
  // If the sensor has locked up it will return a 0 distance. If that happens try resetting it and re-reading...
  if (distance == 0 && digitalRead(ECHO_PIN) == HIGH) {
    lcd.setCursor( 0, 0 );
    lcd.print("In Error Path   ");
    lcd.setCursor( 0, 1 );
    lcd.print(blank13);
    delay(5000);
    errorcount++;
    Serial.println("Houston, We Have A Problem...Trying to reboot the sensor!");
    lcd.setCursor( 0, 0 );
    lcd.print("Sensor Error!   ");
    lcd.setCursor( 0, 1 );
    lcd.print("Trying Reset!");
    lcd.setCursor( 14, 1 );
    lcd.print(errorcount);
    pinMode(ECHO_PIN, OUTPUT);
    digitalWrite(ECHO_PIN, LOW);
    delay(100);
    pinMode(ECHO_PIN, INPUT);
    distance = sonar.ping_in();
    Serial.print("Single/Median Ping Reading: ");
    Serial.print(distance);
    distance = sonar.convert_in(sonar.ping_median(5));
    Serial.print("/");
    Serial.println(distance);
    delay(5000);
    lcd.setCursor( 0, 1 );
    lcd.print(blank13);
  }
  else {
    lcd.setCursor( 0, 0 );
    lcd.print("Sensor Good!    ");
    lcd.setCursor( 0, 1 );
    lcd.print(blank13);
    delay(2000);
    /*
        This is the maximum height of the water less the caclulated sensor reading in inches less the air space
        in the top of the tank plus (or minus) any calibration fudge factor
    */
    waterheight = maxheight - ((distance - airspace) - fudgefactor);
    /*
        If the distance is equal to or greater than the depth then the tank is empty so log that and skip the calcs
    */
    if (waterheight >= maxheight) {
      waterheight = maxheight;
      gallons = capacity;
    }
    else {
      percent = (float)waterheight / (float)maxheight;
      gallons = (float)capacity * percent;
    }
    // Only update the DB if something has changed since the last read. Cuts down on DB Rows...
    if (priorgallons != gallons) {
      char INSERT_SQL[] = "INSERT INTO welldb.well_tank (inches, gallons) VALUES (%d, %d);";
      char query[255];
      sprintf(query, INSERT_SQL, waterheight, gallons);
      // Initiate the query class instance
      MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
      lcd.setCursor( 0, 0 );
==>   lcd.print("Writing To DB   ");
      lcd.setCursor( 0, 1 );
      lcd.print(blank13);
      delay(2000);
      // Execute the query
      if (client.connected()) {
        cur_mem->execute(query);
        // Since this is an insert do not need to read any data,
        // But maybe this should check for an error condition
        // Delete the cursor to free up memory
        delete cur_mem;
        priorgallons = gallons;
        lcd.setCursor( 0, 0 );
        lcd.print("DB Write Done   ");
        lcd.setCursor( 0, 1 );
        lcd.print(blank13);
        delay(2000);
      }
      else {
        lcd.setCursor( 0, 0 );
        lcd.print("Ethernet Error! ");
        lcd.setCursor( 0, 1 );
        lcd.print("Trying Recon!");
        delay(2000);
        if (!lanConnect()) {
           lcd.setCursor( 0, 0 );
           lcd.print("Fatal LAN Error!");
           lcd.setCursor( 0, 1 );
           lcd.print("Reset Needed!");
           delay(10000);
        }
      }
    }
    else {
      lcd.setCursor( 0, 0 );
      lcd.print("No Data Changed ");
      lcd.setCursor( 0, 1 );
      lcd.print("Skipping DB  ");
      delay(2000);
      lcd.setCursor( 0, 1 );
      lcd.print(blank13);
    }
  }
  lcd.setCursor( 0, 0 );
  lcd.print("Gal  In NxtRd Er");
  lcd.setCursor( 0, 1 );
  lcd.print(gallons);
  lcd.setCursor( 5, 1 );
  lcd.print(waterheight);
  lcd.setCursor(8, 1);
  int counter = waittime * 60;
  // Wait time is done this way to prevent overflowing integer values and hanging the app.
  for (int i = 1; i <= waittime; i++) {
    for (int j = 1; j <= 60; j++) {
      lcd.setCursor(8, 1);
      lcd.print("     ");      // Used to clear out overlay digits on the display leaving the last 2 chars for errors
      delay(10);
      lcd.setCursor(8, 1);
      lcd.print(counter);
      delay(990);
      counter = counter - 1;
    }
  }
}
madmacks59 commented 5 years ago

I just saw the details in issue #85 and will add the flush statements you mentioned into the .cpp file and see if that helps.

madmacks59 commented 5 years ago

I've opened MySQL_Cursor.cpp (Ver 1.1.1a) and MySQL_Cursor.cpp (Ver 1.1.1a) and I'd like to confirm the line references you want the Flush statements add after.

In MySQL_Cursor.cpp you say insert at line 140, and that looks like a valid location. In MySQL_Packet.cpp you say insert at line 158, but that definitely doesn't look reasonable (hanging outside the surrounding functions).

Here's a cut/paste from the other issue (#85)

MySQL_Cursor.cpp @ line#140
...
conn->client->write((uint8_t*)conn->buffer, query_len + 5);
conn->client->flush();
...
And in MySQL_Packet.cpp @ line#158
...
client->write((uint8_t*)buffer, size_send);
client->flush();
...

Can you confirm you're referencing versions 1.1.1a of the library files? And would it be possible to add a few previous/post lines of code from the libraries wrapping the new statements so I can be sure I'm adding the code in the correct place?

madmacks59 commented 5 years ago

Inadvertently closed...so reopening...

ChuckBell commented 5 years ago

I see a couple of things here. First, I have to tell you that the connector is not guaranteed to work with any variant of MySQL. It was not designed to support anything other than Oracle's MySQL. In fact, I cannot help with any issues directly related to using a non-Oracle variant of MySQL.

That said, the common practice in long-running sketches is to open and close the connection to MySQL only when you need it. So, move the connect to inside the loop() and use it only when you must read/write the data from/to MySQL. That should help greatly in stability (unless the problem is in the variant code).

Second, this is the diff for the 1.1.1a code:

diff --git a/src/MySQL_Cursor.cpp b/src/MySQL_Cursor.cpp index 3f7b39d..ba6641b 100644 --- a/src/MySQL_Cursor.cpp +++ b/src/MySQL_Cursor.cpp @@ -138,6 +138,7 @@ boolean MySQL_Cursor::execute_query(int query_len)

// Send the query conn->client->write((uint8_t*)conn->buffer, query_len + 5);

Use this as a guide to make the changes. In other words, add only those lines with '+' (minus the +) at the location described by the context (lines without markings). As you can see, the flush() calls are after the write() calls.

ChuckBell commented 5 years ago

Hmmm... + came out as a dot. So, use the dot, Luke...

madmacks59 commented 5 years ago

LOL...I will master...

Couple of things. I will try (do, not try) and update the .cpp files using the diff info you provided, which is greatly appreciated. And, I will start looking for an Oracle MySQL package for my server. I’m running “MySQL” on a RaspberPi and AFAIKT the package built for Pi doesn’t use “My” but rather “Maria”, and as we all know daughters aren’t identical at a genetic level even if they have the same parents.

Most people I’ve checked with say “but they’re functionally identical” and “Maria is even better” but I wonder if, at the binary level, that is in fact true. And I suppose your drivers work at the lower level and not at the call level.

Also, I’ll adopt the processing changes you recommend. I’m new to the “Arduino” world so best practices on that platform are new to me. I’m used to opening DB connections and maintaining them for the life of the program so as to cut down on overhead calls, but I guess on my set up that’s really not a big deal and if the recommended changes improves stability I’m all for it.

Thanks again, and I’ll post back my results...

And, thank you very!

madmacks59 commented 5 years ago

So, I updated MySQL_Cursor.cpp as follows:

boolean MySQL_Cursor::execute_query(int query_len) { ... //// Send the query (ORIGINAL BLOCK) //for (int c = 0; c < query_len+5; c++) // conn->client->write(conn->buffer[c]);

// Send the query (NEW BLOCK) for (int c = 0; c < query_len+5; c++) { conn->client->write(conn->buffer[c]); conn->client->flush(); } ... }

and MySQL_Packet.cpp

//// Write the packet (ORIGINAL PACKET) //for (int i = 0; i < size_send; i++) // client->write(buffer[i]);

// Write the packet (NEW PACKET) for (int i = 0; i < size_send; i++) { client->write(buffer[i]); client->flush(); }

These changes didn't seem to help stabilize the program and I still had DB lock ups as the program ran over longer periods of time.

I then made the code changes you recommended by moving the db connect into the main loop and now connect and disconnect each time I write to the DB. This seems to help a lot, but I still connection fails. I am able to trap these connection problems in the logic and try to reset things and keep processing. I also added a set of counters to track sensor errors and DB connect/write errors. When a set threshold of errors is reached I am now trying to reset the Arduino (and it's shields) via software. Because it's a software reset I'm not sure it will resolve all issues, but it's a step in a better direction. If that doesn't work I'll work on setting up a triggerable power reset that will cut and reset power via a pin trigger on the Arduino. I'll post my updated code once I get a set of better code runs completed.

I've also started trying to find a stable build of "Oracle" MySQL for my Raspberry Pi so I can try to eliminate MariaDB as a possible source of instability. But I do have one request/thought. As most of the open source world has left the "Oracle fork" of MySQL wouldn't it be a good idea to look at supporting MariaDB officially? I know that Raspbian and many other Linux derivatives now use MariaDB rather than Oracle... Just a thought, and thank you for your "unofficial support" and suggestions; they're really appreciated!

ChuckBell commented 5 years ago

I’ll try and reason out some suggestions later, but PM me about the Oracle MySQL issue if you’d like. d r c h a r l e s b e l l at g m a i l dot c o m

On Mar 21, 2019, at 6:51 PM, madmacks59 notifications@github.com wrote:

So, I updated MySQL_Cursor.cpp as follows:

boolean MySQL_Cursor::execute_query(int query_len) { ... //// Send the query (ORIGINAL BLOCK) //for (int c = 0; c < query_len+5; c++) // conn->client->write(conn->buffer[c]);

// Send the query (NEW BLOCK) for (int c = 0; c < query_len+5; c++) { conn->client->write(conn->buffer[c]); conn->client->flush(); } ... }

and MySQL_Packet.cpp

//// Write the packet (ORIGINAL PACKET) //for (int i = 0; i < size_send; i++) // client->write(buffer[i]);

// Write the packet (NEW PACKET) for (int i = 0; i < size_send; i++) { client->write(buffer[i]); client->flush(); }

These changes didn't seem to help stabilize the program and I still had DB lock ups as the program ran over longer periods of time.

I then made the code changes you recommended by moving the db connect into the main loop and now connect and disconnect each time I write to the DB. This seems to help a lot, but I still connection fails. I am able to trap these connection problems in the logic and try to reset things and keep processing. I also added a set of counters to track sensor errors and DB connect/write errors. When a set threshold of errors is reached I am now trying to reset the Arduino (and it's shields) via software. Because it's a software reset I'm not sure it will resolve all issues, but it's a step in a better direction. If that doesn't work I'll work on setting up a triggerable power reset that will cut and reset power via a pin trigger on the Arduino. I'll post my updated code once I get a set of better code runs completed.

I've also started trying to find a stable build of "Oracle" MySQL for my Raspberry Pi so I can try to eliminate MariaDB as a possible source of instability. But I do have one request/thought. As most of the open source world has left the "Oracle fork" of MySQL wouldn't it be a good idea to look at supporting MariaDB officially? I know that Raspbian and many other Linux derivatives now use MariaDB rather than Oracle... Just a thought, and thank you for your "unofficial support" and suggestions; they're really appreciated!

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ChuckBell/MySQL_Connector_Arduino/issues/93#issuecomment-475432604, or mute the thread https://github.com/notifications/unsubscribe-auth/AH0j4OL2_TkQXMxm9-Pw2yvnuAEFBVHeks5vZA0OgaJpZM4b3fPA.

ChuckBell commented 5 years ago

I am glad my suggestions have helped and I think you're on the right track. The software or watchdog reset has worked for me in those cases where I want the Arduino to restart in the event of a catastrophic failure (like dropping connections, slow/contested network, power, etc.). Please keep us posted on your progress.

On 3/21/19 6:51 PM, madmacks59 wrote:

So, I updated MySQL_Cursor.cpp as follows:

boolean MySQL_Cursor::execute_query(int query_len) { ... //// Send the query (ORIGINAL BLOCK) //for (int c = 0; c < query_len+5; c++) // conn->client->write(conn->buffer[c]);

// Send the query (NEW BLOCK) for (int c = 0; c < query_len+5; c++) { conn->client->write(conn->buffer[c]); conn->client->flush(); } ... }

and MySQL_Packet.cpp

//// Write the packet (ORIGINAL PACKET) //for (int i = 0; i < size_send; i++) // client->write(buffer[i]);

// Write the packet (NEW PACKET) for (int i = 0; i < size_send; i++) { client->write(buffer[i]); client->flush(); }

These changes didn't seem to help stabilize the program and I still had DB lock ups as the program ran over longer periods of time.

I then made the code changes you recommended by moving the db connect into the main loop and now connect and disconnect each time I write to the DB. This seems to help a lot, but I still connection fails. I am able to trap these connection problems in the logic and try to reset things and keep processing. I also added a set of counters to track sensor errors and DB connect/write errors. When a set threshold of errors is reached I am now trying to reset the Arduino (and it's shields) via software. Because it's a software reset I'm not sure it will resolve all issues, but it's a step in a better direction. If that doesn't work I'll work on setting up a triggerable power reset that will cut and reset power via a pin trigger on the Arduino. I'll post my updated code once I get a set of better code runs completed.

I've also started trying to find a stable build of "Oracle" MySQL for my Raspberry Pi so I can try to eliminate MariaDB as a possible source of instability. But I do have one request/thought. As most of the open source world has left the "Oracle fork" of MySQL wouldn't it be a good idea to look at supporting MariaDB officially? I know that Raspbian and many other Linux derivatives now use MariaDB rather than Oracle... Just a thought, and thank you for your "unofficial support" and suggestions; they're really appreciated!

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ChuckBell/MySQL_Connector_Arduino/issues/93#issuecomment-475432604, or mute the thread https://github.com/notifications/unsubscribe-auth/AH0j4OL2_TkQXMxm9-Pw2yvnuAEFBVHeks5vZA0OgaJpZM4b3fPA.

macpelos commented 4 years ago

Hi there Something like that here. The only thing I've checked looking at mysql logs is that there is no proper connection closing even calling close() method. The connection is closed for the client, but I cannot see a "Quit" in the server logs as I see if I do the same with php. After a while, a cascade of aborted connections in the logs that, surprisingly, do their thing (INSERT in my case). I think the problem is to send that "quit" packet to the server, but my code skills are not enough to build that by myself. Or maybe I'm completely wrong... Maybe if you, Dr. Bell, or anyone else could guide me I'll try to do it. Regards

ChuckBell commented 4 years ago

Hi. I suggest looking at the send_authentication_packet() is MySQL_Packet.h/.cpp and similar methods.

On Jan 6, 2020, at 7:39 AM, Miguel Ángel Contreras notifications@github.com wrote:

Hi there Something like that here. The only thing I've checked looking at mysql logs is that there is no proper connection closing even calling close() method. The connection is closed for the client, but I cannot see a "Quit" in the server logs as I see if I do the same with php. After a while, a cascade of aborted connections in the logs that, surprisingly, do their thing (INSERT in my case). I think the problem is to send that "quit" packet to the server, but my code skills are not enough to build that by myself. Or maybe I'm completely wrong... Maybe if you, Dr. Bell, or anyone else could guide me I'll try to do it. Regards

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ChuckBell/MySQL_Connector_Arduino/issues/93?email_source=notifications&email_token=AB6SHYGOCOD7CUV3FFUVSFTQ4MRAVA5CNFSM4G656PAKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIFKQNY#issuecomment-571123767, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB6SHYG3JGF3YPQCOZ6V6FDQ4MRAVANCNFSM4G656PAA.