Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset db connection before returning it to the pool #6407

Closed
wants to merge 3 commits into from

Conversation

jellonek
Copy link
Contributor

Motivation

We have seen in logs:

http: panic serving 127.0.0.1:34652: connection returned to pool has active statement: "select address, balance, next_nonce, max(layer_updated), template, state from accounts group by address order by layer_updated desc limit 100"
as part of http handler recovery function is returning connection to the pool, while it can have pending, non finished statement.

Code in this patch should fix this issue.

Test Plan

No new unittests added as the original issue was not reproduced.

TODO

  • Explain motivation or link existing issue(s)
  • Test changes and document test plan
  • Update documentation as needed
  • Update changelog as needed

We have seen in logs:
> http: panic serving 127.0.0.1:34652: connection returned to pool has active statement: "select address, balance, next_nonce, max(layer_updated), template, state from accounts group by address order by layer_updated desc limit 100"
as part of http handler recovery function is returning connection to the
pool, while it can have pending, non finished statement.
Code in this patch should fix this issue.
sql/database.go Outdated
@@ -605,6 +605,15 @@ func (db *sqliteDatabase) getConn(ctx context.Context) *sqlite.Conn {
return conn
}

func (db *sqliteDatabase) returnConn(conn *sqlite.Conn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe putConn given that we have getConn already and we're dealing with a pool with its "get"/"put" operations?
Also returnConn sounds a bit misleading as it may sound as if it's dealing with return value of some kind

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In IT we have 2 main issues - naming and cache invalidation ;)
I was thinking too long on how to name this func :D but i'll take your suggestion.

if conn.CheckReset() != "" {
ch := make(chan struct{})
conn.SetInterrupt(ch)
close(ch)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs some comments explaining why it needs to happen this way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment with explanation added.

Copy link
Member

@fasmat fasmat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests that fail without your change but pass with the adapted code?

@fasmat
Copy link
Member

fasmat commented Oct 23, 2024

Also: this feels like a hack that is necessary, because we might not be using contexts correctly in database.go.

For instance: I feel like withTx should look like this:

func (db *sqliteDatabase) withTx(ctx context.Context, initstmt string, exec func(Transaction) error) (err error) {
	txCtx, cancel := context.WithCancel()
	defer cancel()
	tx, err := db.getTx(txCtx, initstmt)
	if err != nil {
		return err
	}
	defer func() {
		if rErr := tx.Release(); rErr != nil && err == nil {
			err = fmt.Errorf("release tx: %w", rErr)
		}
	}()
	if err := exec(tx); err != nil {
		tx.queryCache.ClearCache()
		return err
	}
	return tx.Commit()
}

If now the exec function panics the connection should be returned to the pool as expected, even if the caller passes context.Background into WithTx without having to adjust tx.Release().

@fasmat
Copy link
Member

fasmat commented Oct 23, 2024

I just thought of a better approach than my previous suggestion: withTx doesn't need to be modified at all if getTx and sqliteTx are adapted:

func (db *sqliteDatabase) getTx(ctx context.Context, initstmt string) (*sqliteTx, error) {
	if db.closed {
		return nil, ErrClosed
	}
	conCtx, cancel := context.WithCancel(ctx)
	conn := db.getConn(conCtx)
	if conn == nil {
		cancel()
		return nil, ErrNoConnection
	}
	tx := &sqliteTx{queryCache: db.queryCache, db: db, conn: conn, freeConn: cancel}
	if err := tx.begin(initstmt); err != nil {
		cancel()
		// TODO: shouldn't the TX also be put back into the pool here?
		return nil, err
	}
	return tx, nil
}

// sqliteTx is wrapper for database transaction.
type sqliteTx struct {
	*queryCache

	db        *sqliteDatabase
	conn      *sqlite.Conn
	freeConn  func()
	committed bool
	err       error
}

// Release transaction. Every transaction that was created must be released.
func (tx *sqliteTx) Release() error {
	tx.freeConn()
	defer tx.db.pool.Put(tx.conn)
	if tx.committed {
		return nil
	}
	stmt := tx.conn.Prep("ROLLBACK")
	_, tx.err = stmt.Step()
	return mapSqliteError(tx.err)
}

Copy link

codecov bot commented Oct 23, 2024

Codecov Report

Attention: Patch coverage is 38.88889% with 11 lines in your changes missing coverage. Please review.

Project coverage is 79.7%. Comparing base (55c3547) to head (dc2f28e).

Files with missing lines Patch % Lines
sql/database.go 38.8% 10 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           develop   #6407     +/-   ##
=========================================
- Coverage     79.8%   79.7%   -0.1%     
=========================================
  Files          328     328             
  Lines        42879   42893     +14     
=========================================
- Hits         34221   34201     -20     
- Misses        6725    6758     +33     
- Partials      1933    1934      +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jellonek
Copy link
Contributor Author

IMO your proposed code will fail as cancel func is nowhere passed so it can not be called when needed - before returning connection to the pool.
IMO that would work if we would store cancel func on db struct then call it before calling db.pool.Put() in any defer.

@jellonek
Copy link
Contributor Author

Closing in favor of more clean approach proposed in #6408

@jellonek jellonek closed this Oct 23, 2024
@jellonek jellonek deleted the returningconnections branch October 23, 2024 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants