github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/server/mysql/mysql.go (about)

     1  // Copyright 2017 Google Inc.
     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  //     https://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 mysql implements the fleetspeak datastore interface using a mysql
    16  // database.
    17  //
    18  // NOTE: Currently this is a fairly direct port of the sqlite datastore and not
    19  // at all performant. TODO: Optimize and load test this.
    20  package mysql
    21  
    22  import (
    23  	"context"
    24  	"database/sql"
    25  	"time"
    26  
    27  	"github.com/go-sql-driver/mysql"
    28  	log "github.com/golang/glog"
    29  )
    30  
    31  const maxRetries = 50
    32  
    33  const mysql_ER_LOCK_WAIT_TIMEOUT = 1205
    34  const mysql_ER_LOCK_DEADLOCK = 1213
    35  const mysql_ER_TOO_MANY_CONCURRENT_TRXS = 1637
    36  
    37  // Datastore wraps a mysql backed sql.DB and implements db.Store.
    38  type Datastore struct {
    39  	db     *sql.DB
    40  	looper *messageLooper
    41  }
    42  
    43  // MakeDatastore creates any missing tables and returns a Datastore. The db
    44  // parameter must be connected to a mysql database, e.g. using the mymysql
    45  // driver.
    46  func MakeDatastore(db *sql.DB) (*Datastore, error) {
    47  	err := initDB(db)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	err = initSchema(db)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return &Datastore{db: db}, nil
    56  }
    57  
    58  // Close closes the underlying database resources.
    59  func (d *Datastore) Close() error {
    60  	return d.db.Close()
    61  }
    62  
    63  // runOnce runs f, passing it a transaction. The transaction will be committed
    64  // if f returns nil, otherwise rolled back.
    65  func (d *Datastore) runOnce(ctx context.Context, readOnly bool, f func(*sql.Tx) error) error {
    66  	// TODO: Pass along the readOnly flag, once some mysql driver supports it.
    67  	tx, err := d.db.BeginTx(ctx, nil)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	err = f(tx)
    72  	if err != nil {
    73  		tx.Rollback()
    74  		return err
    75  	}
    76  	return tx.Commit()
    77  }
    78  
    79  // runInTx runs f in a transaction. If the transaction returns a retriable MySQL
    80  // error, the transaction will be retried (and f will be run again).
    81  func (d *Datastore) runInTx(ctx context.Context, readOnly bool, f func(*sql.Tx) error) error {
    82  	var err error
    83  	for i := range maxRetries {
    84  		err = d.runOnce(ctx, readOnly, f)
    85  		e, ok := err.(*mysql.MySQLError)
    86  		if !ok {
    87  			return err
    88  		}
    89  		switch e.Number {
    90  		case mysql_ER_LOCK_WAIT_TIMEOUT, mysql_ER_LOCK_DEADLOCK, mysql_ER_TOO_MANY_CONCURRENT_TRXS:
    91  			t := time.NewTimer(time.Duration(i*100) * time.Millisecond)
    92  			select {
    93  			case <-t.C:
    94  				continue
    95  			case <-ctx.Done():
    96  				t.Stop()
    97  				return err
    98  			}
    99  		default:
   100  			return err
   101  		}
   102  	}
   103  	return err
   104  }
   105  
   106  // IsNotFound implements db.Store.
   107  func (d *Datastore) IsNotFound(err error) bool {
   108  	return err == sql.ErrNoRows
   109  }
   110  
   111  func initDB(db *sql.DB) error {
   112  	db.SetMaxIdleConns(100)
   113  	db.SetMaxOpenConns(100)
   114  	return nil
   115  }
   116  
   117  func initSchema(db *sql.DB) error {
   118  	for _, s := range []string{
   119  		`CREATE TABLE IF NOT EXISTS clients(
   120  client_id BINARY(8) PRIMARY KEY,
   121  client_key BLOB,
   122  blacklisted BOOL NOT NULL,
   123  last_contact_time BIGINT NOT NULL,
   124  last_contact_address TEXT(64),
   125  last_contact_streaming_to TEXT(128),
   126  last_clock_seconds BIGINT UNSIGNED,
   127  last_clock_nanos INT UNSIGNED)`,
   128  		`CREATE TABLE IF NOT EXISTS client_labels(
   129  client_id BINARY(8) NOT NULL,
   130  service_name VARCHAR(128) NOT NULL,
   131  label VARCHAR(128) NOT NULL,
   132  PRIMARY KEY (client_id, service_name, label),
   133  CONSTRAINT client_labels_fk_1
   134    FOREIGN KEY (client_id)
   135    REFERENCES clients(client_id)
   136    ON DELETE CASCADE)`,
   137  		`CREATE TABLE IF NOT EXISTS client_contacts(
   138  client_contact_id INTEGER NOT NULL AUTO_INCREMENT,
   139  client_id BINARY(8) NOT NULL,
   140  time BIGINT NOT NULL,
   141  sent_nonce BINARY(8) NOT NULL,
   142  received_nonce BINARY(8) NOT NULL,
   143  address VARCHAR(64),
   144  PRIMARY KEY (client_contact_id),
   145  CONSTRAINT client_contacts_fk_1
   146    FOREIGN KEY (client_id)
   147    REFERENCES clients(client_id)
   148    ON DELETE CASCADE)`,
   149  		`CREATE TABLE IF NOT EXISTS client_resource_usage_records(
   150  client_id BINARY(8) NOT NULL,
   151  scope VARCHAR(128) NOT NULL,
   152  pid BIGINT,
   153  process_start_time BIGINT,
   154  client_timestamp BIGINT,
   155  server_timestamp BIGINT,
   156  process_terminated BOOL NOT NULL,
   157  mean_user_cpu_rate REAL,
   158  max_user_cpu_rate REAL,
   159  mean_system_cpu_rate REAL,
   160  max_system_cpu_rate REAL,
   161  mean_resident_memory_mib INT4,
   162  max_resident_memory_mib INT4,
   163  mean_num_fds INT,
   164  max_num_fds INT,
   165  CONSTRAINT client_resource_usage_records_fk_1
   166    FOREIGN KEY (client_id) 
   167    REFERENCES clients(client_id)
   168    ON DELETE CASCADE)`,
   169  		`CREATE TABLE IF NOT EXISTS messages(
   170  message_id BINARY(32) NOT NULL,
   171  source_client_id BINARY(8),
   172  source_service_name VARCHAR(128) NOT NULL,
   173  source_message_id VARBINARY(16) NULL,
   174  destination_client_id BINARY(8),
   175  destination_service_name VARCHAR(128) NOT NULL,
   176  message_type VARCHAR(128),
   177  creation_time_seconds BIGINT NOT NULL,
   178  creation_time_nanos INT NOT NULL,
   179  processed_time_seconds BIGINT,
   180  processed_time_nanos INT,
   181  validation_info BLOB,
   182  failed INT1,
   183  failed_reason TEXT,
   184  annotations BLOB,
   185  PRIMARY KEY (message_id))`,
   186  		`CREATE TABLE IF NOT EXISTS pending_messages(
   187  for_server BOOL NOT NULL,
   188  message_id BINARY(32) NOT NULL,
   189  retry_count INT NOT NULL,
   190  scheduled_time BIGINT NOT NULL,
   191  data_type_url TEXT,
   192  data_value MEDIUMBLOB,
   193  PRIMARY KEY (for_server, message_id),
   194  CONSTRAINT pending_messages_fk_1
   195    FOREIGN KEY (message_id)
   196    REFERENCES messages(message_id)
   197    ON DELETE CASCADE)`,
   198  		`CREATE TABLE IF NOT EXISTS client_contact_messages(
   199  client_contact_id INTEGER NOT NULL,
   200  message_id BINARY(32) NOT NULL,
   201  PRIMARY KEY (client_contact_id, message_id),
   202  CONSTRAINT client_contact_messages_fk_1
   203    FOREIGN KEY (client_contact_id)
   204    REFERENCES client_contacts(client_contact_id)
   205    ON DELETE CASCADE,
   206  CONSTRAINT client_contact_messages_fk_2
   207    FOREIGN KEY (message_id)
   208    REFERENCES messages(message_id)
   209    ON DELETE CASCADE)`,
   210  		`CREATE TABLE IF NOT EXISTS broadcasts(
   211  broadcast_id BINARY(8) NOT NULL,
   212  source_service_name VARCHAR(128) NOT NULL,
   213  message_type VARCHAR(128) NOT NULL,
   214  expiration_time_seconds BIGINT,
   215  expiration_time_nanos INT,
   216  data_type_url TEXT,
   217  data_value MEDIUMBLOB,
   218  sent BIGINT UNSIGNED,
   219  allocated BIGINT UNSIGNED,
   220  message_limit BIGINT UNSIGNED,
   221  PRIMARY KEY (broadcast_id))`,
   222  		`CREATE TABLE IF NOT EXISTS broadcast_labels(
   223  broadcast_id BINARY(8) NOT NULL,
   224  service_name VARCHAR(128) NOT NULL,
   225  label VARCHAR(128) NOT NULL,
   226  PRIMARY KEY (broadcast_id, service_name, label),
   227  CONSTRAINT broadcast_labels_fk_1
   228    FOREIGN KEY (broadcast_id)
   229    REFERENCES broadcasts(broadcast_id)
   230    ON DELETE CASCADE)`,
   231  		`CREATE TABLE IF NOT EXISTS broadcast_allocations(
   232  broadcast_id BINARY(8) NOT NULL,
   233  allocation_id BINARY(8) NOT NULL,
   234  sent BIGINT UNSIGNED,
   235  message_limit BIGINT UNSIGNED,
   236  expiration_time_seconds BIGINT,
   237  expiration_time_nanos INT,
   238  PRIMARY KEY (broadcast_id, allocation_id),
   239  CONSTRAINT broadcast_allocations_fk_1
   240    FOREIGN KEY (broadcast_id)
   241    REFERENCES broadcasts(broadcast_id)
   242    ON DELETE CASCADE)`,
   243  		`CREATE TABLE IF NOT EXISTS broadcast_sent(
   244  broadcast_id BINARY(8) NOT NULL,
   245  client_id BINARY(8) NOT NULL,
   246  PRIMARY KEY (client_id, broadcast_id),
   247  CONSTRAINT broadcast_sent_fk_1
   248    FOREIGN KEY (broadcast_id)
   249    REFERENCES broadcasts(broadcast_id)
   250    ON DELETE CASCADE,
   251  CONSTRAINT broadcast_sent_fk_2
   252    FOREIGN KEY (client_id)
   253    REFERENCES clients(client_id)
   254    ON DELETE CASCADE)`,
   255  		`CREATE TABLE IF NOT EXISTS files(
   256  service VARCHAR(128) NOT NULL,
   257  name VARCHAR(128) NOT NULL,
   258  modified_time_nanos BIGINT NOT NULL,
   259  data MEDIUMBLOB,
   260  PRIMARY KEY (service, name))
   261  `,
   262  	} {
   263  		if _, err := db.Exec(s); err != nil {
   264  			log.Errorf("Error [%v] creating table: \n%v", err, s)
   265  			return err
   266  		}
   267  	}
   268  
   269  	return nil
   270  }