github.com/letsencrypt/trillian@v1.1.2-0.20180615153820-ae375a99d36a/quota/mysqlqm/mysql_quota.go (about) 1 // Copyright 2017 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package mysqlqm defines a MySQL-based quota.Manager implementation. 16 package mysqlqm 17 18 import ( 19 "context" 20 "database/sql" 21 "errors" 22 23 "github.com/google/trillian/quota" 24 ) 25 26 const ( 27 // DefaultMaxUnsequenced is a suggested value for MaxUnsequencedRows. 28 // Note that this is a Global/Write quota suggestion, so it applies across trees. 29 DefaultMaxUnsequenced = 500000 // About 2h of non-stop signing at 70QPS. 30 31 countFromInformationSchemaQuery = ` 32 SELECT table_rows 33 FROM information_schema.tables 34 WHERE table_schema = schema() 35 AND table_name = ? 36 AND table_type = ?` 37 countFromUnsequencedQuery = "SELECT COUNT(*) FROM Unsequenced" 38 ) 39 40 var ( 41 // ErrTooManyUnsequencedRows is returned when tokens are requested but Unsequenced has grown 42 // beyond the configured limit. 43 ErrTooManyUnsequencedRows = errors.New("too many unsequenced rows") 44 ) 45 46 // QuotaManager is a MySQL-based quota.Manager implementation. 47 // 48 // It has two working modes: one queries the information schema for the number of Unsequenced rows, 49 // the other does a select count(*) on the Unsequenced table. Information schema queries are 50 // default, even though they are approximate, as they're constant time (select count(*) on InnoDB 51 // based MySQL needs to traverse the index and may take quite a while to complete). 52 // 53 // QuotaManager only implements Global/Write quotas, which is based on the number of Unsequenced 54 // rows (to be exact, tokens = MaxUnsequencedRows - actualUnsequencedRows). 55 // Other quotas are considered infinite. 56 type QuotaManager struct { 57 DB *sql.DB 58 MaxUnsequencedRows int 59 UseSelectCount bool 60 } 61 62 // GetUser implements quota.Manager.GetUser. 63 // User quotas are not implemented by QuotaManager. 64 func (m *QuotaManager) GetUser(ctx context.Context, req interface{}) string { 65 return "" // Not used 66 } 67 68 // GetTokens implements quota.Manager.GetTokens. 69 // It doesn't actually reserve or retrieve tokens, instead it allows access based on the number of 70 // rows in the Unsequenced table. 71 func (m *QuotaManager) GetTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { 72 for _, spec := range specs { 73 if spec.Group != quota.Global || spec.Kind != quota.Write { 74 continue 75 } 76 // Only allow global writes if Unsequenced is under the expected limit 77 count, err := m.countUnsequenced(ctx) 78 if err != nil { 79 return err 80 } 81 if count+numTokens > m.MaxUnsequencedRows { 82 return ErrTooManyUnsequencedRows 83 } 84 } 85 return nil 86 } 87 88 // PeekTokens implements quota.Manager.PeekTokens. 89 // Global/Write tokens reflect the number of rows in the Unsequenced tables, other specs are 90 // considered infinite. 91 func (m *QuotaManager) PeekTokens(ctx context.Context, specs []quota.Spec) (map[quota.Spec]int, error) { 92 tokens := make(map[quota.Spec]int) 93 for _, spec := range specs { 94 var num int 95 if spec.Group == quota.Global && spec.Kind == quota.Write { 96 count, err := m.countUnsequenced(ctx) 97 if err != nil { 98 return nil, err 99 } 100 num = m.MaxUnsequencedRows - count 101 } else { 102 num = quota.MaxTokens 103 } 104 tokens[spec] = num 105 } 106 return tokens, nil 107 } 108 109 // PutTokens implements quota.Manager.PutTokens. 110 // It's a noop for QuotaManager. 111 func (m *QuotaManager) PutTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { 112 return nil 113 } 114 115 // ResetQuota implements quota.Manager.ResetQuota. 116 // It's a noop for QuotaManager. 117 func (m *QuotaManager) ResetQuota(ctx context.Context, specs []quota.Spec) error { 118 return nil 119 } 120 121 func (m *QuotaManager) countUnsequenced(ctx context.Context) (int, error) { 122 if m.UseSelectCount { 123 return countFromTable(ctx, m.DB) 124 } 125 return countFromInformationSchema(ctx, m.DB) 126 } 127 128 func countFromInformationSchema(ctx context.Context, db *sql.DB) (int, error) { 129 // information_schema.tables doesn't have an explicit PK, so let's play it safe and ensure 130 // the cursor returns a single row. 131 rows, err := db.QueryContext(ctx, countFromInformationSchemaQuery, "Unsequenced", "BASE TABLE") 132 if err != nil { 133 return 0, err 134 } 135 defer rows.Close() 136 if !rows.Next() { 137 return 0, errors.New("cursor has no rows after information_schema query") 138 } 139 var count int 140 if err := rows.Scan(&count); err != nil { 141 return 0, err 142 } 143 if rows.Next() { 144 return 0, errors.New("too many rows returned from information_schema query") 145 } 146 return count, nil 147 } 148 149 func countFromTable(ctx context.Context, db *sql.DB) (int, error) { 150 var count int 151 if err := db.QueryRowContext(ctx, countFromUnsequencedQuery).Scan(&count); err != nil { 152 return 0, err 153 } 154 return count, nil 155 }