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

feat: enable multiple db connections with sqlcipher #3507

Merged
merged 1 commit into from
May 23, 2023

Conversation

osmaczko
Copy link
Contributor

@osmaczko osmaczko commented May 18, 2023

@ghost
Copy link

ghost commented May 18, 2023

Pull Request Checklist

  • Have you updated the documentation, if impacted (e.g. docs.status.im)?
  • Have you tested changes with mobile?
  • Have you tested changes with desktop?

@status-im-auto
Copy link
Member

status-im-auto commented May 18, 2023

Jenkins Builds

Click to see older builds (16)
Commit #️⃣ Finished (UTC) Duration Platform Result
✔️ 7df293e #1 2023-05-18 22:22:20 ~2 min linux 📦zip
✔️ 7df293e #1 2023-05-18 22:24:12 ~4 min android 📦aar
✔️ 7df293e #1 2023-05-18 22:24:19 ~4 min ios 📦zip
✖️ 7df293e #1 2023-05-18 22:38:46 ~19 min tests 📄log
✔️ 1a6dca8 #2 2023-05-22 11:25:45 ~2 min linux 📦zip
✔️ 1a6dca8 #2 2023-05-22 11:27:25 ~4 min ios 📦zip
✔️ 1a6dca8 #2 2023-05-22 11:28:00 ~4 min android 📦aar
✖️ 1a6dca8 #2 2023-05-22 11:30:51 ~7 min tests 📄log
✔️ ae25fcf #3 2023-05-22 11:30:15 ~2 min ios 📦zip
✔️ ae25fcf #3 2023-05-22 11:33:32 ~7 min linux 📦zip
✔️ ae25fcf #3 2023-05-22 11:36:18 ~8 min android 📦aar
✖️ ae25fcf #3 2023-05-22 11:36:41 ~5 min tests 📄log
✔️ b0f63a2 #4 2023-05-22 15:52:02 ~2 min linux 📦zip
✔️ b0f63a2 #4 2023-05-22 15:52:26 ~2 min ios 📦zip
✔️ b0f63a2 #4 2023-05-22 15:52:54 ~3 min android 📦aar
✖️ b0f63a2 #4 2023-05-22 16:06:12 ~16 min tests 📄log
Commit #️⃣ Finished (UTC) Duration Platform Result
✔️ cd5b19d #5 2023-05-22 16:07:48 ~2 min linux 📦zip
✔️ cd5b19d #5 2023-05-22 16:08:31 ~2 min ios 📦zip
✔️ cd5b19d #5 2023-05-22 16:09:05 ~3 min android 📦aar
✖️ cd5b19d #5 2023-05-22 16:10:43 ~4 min tests 📄log
✔️ 6b51b7a #6 2023-05-23 08:22:51 ~2 min linux 📦zip
✔️ 6b51b7a #6 2023-05-23 08:23:21 ~2 min ios 📦zip
✔️ 6b51b7a #6 2023-05-23 08:24:15 ~3 min android 📦aar
✔️ 6b51b7a #6 2023-05-23 08:34:26 ~13 min tests 📄log

Copy link
Contributor

@alexjba alexjba left a comment

Choose a reason for hiding this comment

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

Can't wait to see it in the app 🚀
Might need some tweaking in order to improve the speed and write safety.

sqlite/sqlite.go Outdated
return err
}

return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

WAL mode is not set. Is it on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I thought setting it once would be sufficient, as once you set PRAGMA journal_mode=WAL;, the setting is stored in the database file itself. I have learned that even though switching the mode to WAL introduces a persistent change to the database, each new connection must still "opt-in" to using the WAL mode. If a connection does not do this, it defaults to the DELETE journal mode. While this connection can still read from and write to the database, it will not take advantage of the benefits of WAL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

sqlite/sqlite.go Outdated
// This method is used because the 'database/sql' package does not expose 'ConnectHook',
// thereby making it impossible to individually configure each connection.
// Consequently, the connections from the pool can't be properly decrypted, making them unusable.
sqlcipherDriver, ok := db.Driver().(*sqlcipher.SQLiteDriver)
Copy link
Contributor

Choose a reason for hiding this comment

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

There's one other way of doing it without breaking the abstraction:

        driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers()))
	sql.Register(driverName , &sqlcipher.SQLiteDriver{
		ConnectHook: func(conn *sqlcipher.SQLiteConn) error {
			///Running init queries
		},
	})

	db, err := sql.Open(driverName, path)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Smart! I will use it 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

Copy link
Contributor

Choose a reason for hiding this comment

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

With the current configuration on a test project, when trying a few writes at the same time I get database is locked on 10-20% of them. The test is configured to write 1Kb of data per task.

Might need some tweaking in order to get the async writing in a better shape.
Other projects are using the busy_timeout to fix this, but I think we need to make sure every write we do doesn't take more than the timeout value.

mattn/go-sqlite3#274

Copy link
Member

Choose a reason for hiding this comment

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

I've seen the same error before these changes a couple of times too, with the discord import when the app was under heavy load.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a workaround (PRAGMA busy_timeout=60000) to mitigate the "database is locked" errors during concurrent write operations.

This approach retains behavior similar to our current setup with one 'always blocking' connection, which waits as long as needed for the connection to release lock. I believe setting busy_timeout to 60s (aka "infinite" wait) essentially approximates the same behavior.

sqlite/sqlite.go Outdated
// This method is used because the 'database/sql' package does not expose 'ConnectHook',
// thereby making it impossible to individually configure each connection.
// Consequently, the connections from the pool can't be properly decrypted, making them unusable.
sqlcipherDriver, ok := db.Driver().(*sqlcipher.SQLiteDriver)
Copy link
Contributor

Choose a reason for hiding this comment

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

In my tests I found the best performance when using a limited amount of connections and the same number of max idle connections. The peak performance I see on M1 Macs is when using:

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(10)

Not sure it's a coincidence to be the same number as CPU count.

Leaving the max open connection to unlimited will open a new connection when it's needed, and close the idle connections. This has the potential of paying the cost of opening the connection on lots of queries.

The difference I have is (In the app the difference might be smaller because we're probably not throttling the DB at this magnitude, but it gives an idea on how the connection pool works):

32 queries/second with unlimited connections
60662 queries/second with 10 max connections, 10 max idle connections

Copy link
Member

Choose a reason for hiding this comment

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

In my tests I found the best performance when using a limited amount of connections and the same number of max idle connections. The peak performance I see on M1 Macs is when using:

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(10)

Not sure it's a coincidence to be the same number as CPU count.

Yeah this smells like it's supposed to ideally use the number of HW threads :)

Copy link
Member

Choose a reason for hiding this comment

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

Something like this should be used: https://go.dev/play/p/IFnlTmTD6Q

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've done some benchmarking and it seems for my machine the number of maxIdleConns doesn't really matter:

image

@alexjba could you please give it a try on M1? Benchmarking code: https://github.com/osmaczko/status-go-db-perf/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I set maxOpenConns to nproc as suggested and left maxIdleConns as default = 2 (see comment above).

@alexjba
Copy link
Contributor

alexjba commented May 22, 2023

Closing #10723

@osmaczko osmaczko force-pushed the feat/multiple-sqlcipher-connections branch 4 times, most recently from b0f63a2 to cd5b19d Compare May 22, 2023 16:05
Copy link
Contributor

@alexjba alexjba left a comment

Choose a reason for hiding this comment

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

Nice! 🚀

@osmaczko osmaczko merged commit 5777bb4 into develop May 23, 2023
@osmaczko osmaczko deleted the feat/multiple-sqlcipher-connections branch May 23, 2023 12:43
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.

[Performance] enable multiple db connections with sqlcipher
5 participants