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  }