-
Notifications
You must be signed in to change notification settings - Fork 438
PostgreSQL quota manager and storage backend #3644
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
Changes from all commits
21ef4b2
78653c2
9f48d08
b0f7dfe
18cf306
e01fb0b
33451e6
6d88880
74526b6
e3583cd
a167577
c127965
cb7bda2
52f2867
51def32
22f955a
e8fdc37
0f6f593
10d47f8
e617388
a1ccf26
66d2c7f
8218303
92030b3
f68b8b9
1456b20
aff11fc
bdd9faf
7b0ae0a
2846d36
0121720
ba74553
2c5598f
b365aed
aa47b36
607c13a
7a0df88
7ce0258
55f0656
b644630
9ecaa58
8e459da
6698903
9c72070
7c1f8c5
2d5c941
d1a21e1
0eac2ab
965aa91
651b3b5
72964d2
917f362
dd85139
32096c4
8c40e33
28df5c0
286e278
a4ee5cd
1aa5094
0462f62
6e3eb86
d41757e
378779f
8102b6a
be96182
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| --- | ||
| name: Test PostgreSQL | ||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| lint: | ||
| permissions: | ||
| contents: read # for actions/checkout to fetch code | ||
| pull-requests: read # for golangci/golangci-lint-action to fetch pull requests | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||
|
|
||
| - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 | ||
| with: | ||
| go-version-file: go.mod | ||
| check-latest: true | ||
| cache: true | ||
|
|
||
| - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 | ||
| with: | ||
| version: 'v1.55.1' | ||
| args: ./storage/postgresql | ||
|
|
||
| integration-and-unit-tests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||
|
|
||
| - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 | ||
| with: | ||
| go-version-file: go.mod | ||
| check-latest: true | ||
| cache: true | ||
|
|
||
| - name: Build before tests | ||
| run: go mod download && go build ./... | ||
|
|
||
| - name: Run PostgreSQL | ||
| run: docker run --rm -d --name=pgsql -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:latest | ||
|
|
||
| - name: Wait for PostgreSQL | ||
| uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # v3.0.0 | ||
| with: | ||
| timeout_seconds: 15 | ||
| max_attempts: 3 | ||
| retry_on: error | ||
| command: docker exec pgsql psql -U postgres -c "SELECT 1" | ||
|
|
||
| - name: Get PostgreSQL logs | ||
| run: docker logs pgsql 2>&1 | ||
|
|
||
| - name: Run integration tests | ||
| run: ./integration/integration_test.sh | ||
| env: | ||
| TEST_POSTGRESQL_URI: postgresql:///defaultdb?host=localhost&user=postgres&password=postgres | ||
| POSTGRESQL_IN_CONTAINER: true | ||
| POSTGRESQL_CONTAINER_NAME: pgsql | ||
|
|
||
| - name: Run unit tests | ||
| run: go test -v ./storage/postgresql/... ./quota/postgresqlqm/... | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,10 +49,12 @@ import ( | |
| _ "github.com/google/trillian/storage/cloudspanner" | ||
| _ "github.com/google/trillian/storage/crdb" | ||
| _ "github.com/google/trillian/storage/mysql" | ||
| _ "github.com/google/trillian/storage/postgresql" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're following the existing pattern which is the right thing to do. However, I think this pattern is kinda suspect in that it leads to pretty massive binaries. This small change here increases the size of the log server binary by another 10%. I don't expect you to make any code changes in this PR in response to this. But I'm squirting a stream of conciousness into this comment as a TODO to at least raise an issue to discuss this. It could be as simple a solution as docs saying that this binary is the swiss army knife, but if you want a svelte version for a particular environment then copy it and remove the other imports.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an idea: robstradling#5 |
||
|
|
||
| // Load quota providers | ||
| _ "github.com/google/trillian/quota/crdbqm" | ||
| _ "github.com/google/trillian/quota/mysqlqm" | ||
| _ "github.com/google/trillian/quota/postgresqlqm" | ||
| ) | ||
|
|
||
| var ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // Copyright 2024 Trillian Authors. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package postgresqlqm defines a PostgreSQL-based quota.Manager implementation. | ||
| package postgresqlqm | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
|
|
||
| "github.com/google/trillian/quota" | ||
| "github.com/jackc/pgx/v5/pgxpool" | ||
| ) | ||
|
|
||
| const ( | ||
| // DefaultMaxUnsequenced is a suggested value for MaxUnsequencedRows. | ||
| // Note that this is a Global/Write quota suggestion, so it applies across trees. | ||
| DefaultMaxUnsequenced = 500000 // About 2h of non-stop signing at 70QPS. | ||
|
|
||
| countFromExplainOutputQuery = "SELECT count_estimate($1)" | ||
| countFromUnsequencedQuery = "SELECT COUNT(*) FROM Unsequenced" | ||
| ) | ||
|
|
||
| // ErrTooManyUnsequencedRows is returned when tokens are requested but Unsequenced has grown | ||
| // beyond the configured limit. | ||
| var ErrTooManyUnsequencedRows = errors.New("too many unsequenced rows") | ||
|
|
||
| // QuotaManager is a PostgreSQL-based quota.Manager implementation. | ||
| // | ||
| // QuotaManager only implements Global/Write quotas, which is based on the number of Unsequenced | ||
| // rows (to be exact, tokens = MaxUnsequencedRows - actualUnsequencedRows). | ||
| // Other quotas are considered infinite. In other words, it attempts to protect the MMD SLO of all | ||
| // logs in the instance, but it does not make any attempt to ensure fairness, whether per-tree, | ||
| // per-intermediate-CA (in the case of Certificate Transparency), or any other dimension. | ||
| // | ||
| // It has two working modes: one estimates the number of Unsequenced rows by collecting information | ||
| // from EXPLAIN output; the other does a select count(*) on the Unsequenced table. Estimates are | ||
| // default, even though they are approximate, as they're constant time (select count(*) on | ||
| // PostgreSQL needs to traverse the index and may take quite a while to complete). | ||
| // Other estimation methods exist (see https://wiki.postgresql.org/wiki/Count_estimate), but using | ||
| // EXPLAIN output is the most accurate because it "fetches the actual current number of pages in | ||
| // the table (this is a cheap operation, not requiring a table scan). If that is different from | ||
| // relpages then reltuples is scaled accordingly to arrive at a current number-of-rows estimate." | ||
| // (quoting https://www.postgresql.org/docs/current/row-estimation-examples.html) | ||
| type QuotaManager struct { | ||
| DB *pgxpool.Pool | ||
| MaxUnsequencedRows int | ||
| UseSelectCount bool | ||
| } | ||
|
|
||
| // GetTokens implements quota.Manager.GetTokens. | ||
| // It doesn't actually reserve or retrieve tokens, instead it allows access based on the number of | ||
| // rows in the Unsequenced table. | ||
| func (m *QuotaManager) GetTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { | ||
| for _, spec := range specs { | ||
| if spec.Group != quota.Global || spec.Kind != quota.Write { | ||
| continue | ||
| } | ||
| // Only allow global writes if Unsequenced is under the expected limit | ||
| count, err := m.countUnsequenced(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if count+numTokens > m.MaxUnsequencedRows { | ||
| return ErrTooManyUnsequencedRows | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // PutTokens implements quota.Manager.PutTokens. | ||
| // It's a noop for QuotaManager. | ||
| func (m *QuotaManager) PutTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { | ||
| return nil | ||
| } | ||
|
|
||
| // ResetQuota implements quota.Manager.ResetQuota. | ||
| // It's a noop for QuotaManager. | ||
| func (m *QuotaManager) ResetQuota(ctx context.Context, specs []quota.Spec) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (m *QuotaManager) countUnsequenced(ctx context.Context) (int, error) { | ||
| if m.UseSelectCount { | ||
| return countFromTable(ctx, m.DB) | ||
| } | ||
| return countFromExplainOutput(ctx, m.DB) | ||
| } | ||
|
|
||
| func countFromExplainOutput(ctx context.Context, db *pgxpool.Pool) (int, error) { | ||
| var count int | ||
| if err := db.QueryRow(ctx, countFromExplainOutputQuery, "Unsequenced").Scan(&count); err != nil { | ||
| return 0, err | ||
| } | ||
| return count, nil | ||
| } | ||
|
|
||
| func countFromTable(ctx context.Context, db *pgxpool.Pool) (int, error) { | ||
| var count int | ||
| if err := db.QueryRow(ctx, countFromUnsequencedQuery).Scan(&count); err != nil { | ||
| return 0, err | ||
| } | ||
| return count, nil | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.