Files
tasks/schema/sqlite/setup.go
Tim Deeb-SwihartandGitHub be7b5514a5 Reconnect to SQL databases when connections fail (#5926)
## What changed?

Both our PostgreSQL and MySQL database backends will now automatically
reconnect to the database when certain errors occur: all errors chosen
have been experienced when testing this behavior through an AWS Aurora
RDS failover of either MySQL or PostgreSQL.

For both backends we will reconnect when we see:
- `ECONNRESET`
- `ECONNABORTED`
- `ECONNREFUSED`
- `io.EOF`
- `io.ErrUnexpectedEOF`
- `database/sql/driver.ErrBadConn`

for postgres we will also reconnect on the following SQLStates:
- `25006` read-only transaction
- `57P03` cannot connect now
- `0A000` feature not supported, but ONLY when the message is `cannot
set transaction read-write mode during recovery`

for mysql we will also reconnect when we see the following error codes:
- `1040` too many connections 
- `1792` read-only transaction (SQLstate `25006`)
- `1836` running in read-only mode

This logic is easily extensible should we discover more failure modes
over time

## Why?

We've had multiple community reports of Temporal problems during RDS
failover. One part of this is the fact that we wouldn't necessarily
reconnect; we were at the whims of our chosen SQL abstraction's
connection pooling logic.

## How did you test it?

I manually tested this functionality in the presence of repeated RDS
failovers: 

- [x] postgres12 plugin with pq driver
- [x] postgres12 plugin with pgx driver
- [x] mysql plugin

Automated testing will be added to our regular testing pipelines once
our infrastructure friends have added the support I need (it's in
progress)

## Potential risks

We're concerned there's a correctness issue in our PostgreSQL backend
that's related to our behavior during an RDS failover. If we merge this
before I figure out what's going on, we could hide the issue and make it
harder to reproduce.

## Documentation
N/A

## Is hotfix candidate?
Yes?
2024-05-17 16:59:15 -07:00

194 lines
6.7 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package sqlite
import (
"bytes"
"context"
_ "embed"
"fmt"
"io"
enumspb "go.temporal.io/api/enums/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/config"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/metrics"
p "go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/common/persistence/sql"
"go.temporal.io/server/common/persistence/sql/sqlplugin"
"go.temporal.io/server/common/primitives"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/resolver"
)
var (
//go:embed v3/temporal/schema.sql
executionSchema []byte
//go:embed v3/visibility/schema.sql
visibilitySchema []byte
)
// SetupSchema initializes the SQLite schema in an empty database.
//
// Note: this function may receive breaking changes or be removed in the future.
func SetupSchema(cfg *config.SQL) error {
db, err := sql.NewSQLAdminDB(sqlplugin.DbKindUnknown, cfg, resolver.NewNoopResolver(), log.NewNoopLogger(), metrics.NoopMetricsHandler)
if err != nil {
return fmt.Errorf("unable to create SQLite admin DB: %w", err)
}
defer func() { _ = db.Close() }()
return SetupSchemaOnDB(db)
}
// SetupSchemaOnDB initializes the SQLite schema in an empty database using existing DB connection.
//
// Note: this function may receive breaking changes or be removed in the future.
func SetupSchemaOnDB(db sqlplugin.AdminDB) error {
statements, err := p.LoadAndSplitQueryFromReaders([]io.Reader{bytes.NewBuffer(executionSchema)})
if err != nil {
return fmt.Errorf("error loading execution schema: %w", err)
}
for _, stmt := range statements {
if err = db.Exec(stmt); err != nil {
return fmt.Errorf("error executing statement %q: %w", stmt, err)
}
}
statements, err = p.LoadAndSplitQueryFromReaders([]io.Reader{bytes.NewBuffer(visibilitySchema)})
if err != nil {
return fmt.Errorf("error loading visibility schema: %w", err)
}
for _, stmt := range statements {
if err = db.Exec(stmt); err != nil {
return fmt.Errorf("error executing statement %q: %w", stmt, err)
}
}
return nil
}
// NamespaceConfig determines how namespaces should be configured during registration.
//
// Note: this struct may receive breaking changes or be removed in the future.
type NamespaceConfig struct {
// Low level representation of a Namespace used by Temporal persistence drivers.
Detail *persistencespb.NamespaceDetail
// Global Namespaces provide support for replication of Workflow execution across clusters.
IsGlobal bool
}
// CreateNamespaces creates namespaces in the target database without the need to have a running Temporal server.
//
// This exists primarily as a workaround for https://github.com/temporalio/temporal/issues/1336. Namespaces should
// typically be created through the Temporal API either via `tctl` or an SDK client.
//
// Attempting to create a namespace that already exists will be a no-op.
//
// Note: this function may receive breaking changes or be removed in the future.
func CreateNamespaces(cfg *config.SQL, namespaces ...*NamespaceConfig) error {
db, err := sql.NewSQLDB(sqlplugin.DbKindUnknown, cfg, resolver.NewNoopResolver(), log.NewNoopLogger(), metrics.NoopMetricsHandler)
if err != nil {
return fmt.Errorf("unable to create SQLite admin DB: %w", err)
}
defer func() { _ = db.Close() }()
for _, ns := range namespaces {
if err := createNamespaceIfNotExists(db, ns); err != nil {
return fmt.Errorf("error creating namespace %q: %w", ns.Detail.Info.Name, err)
}
}
return nil
}
// NewNamespaceConfig initializes a NamespaceConfig with the field values needed to pre-register
// the namespace via the CreateNamespaces function.
//
// Note: this function may receive breaking changes or be removed in the future.
func NewNamespaceConfig(activeClusterName, namespace string, global bool) *NamespaceConfig {
detail := persistencespb.NamespaceDetail{
Info: &persistencespb.NamespaceInfo{
Id: primitives.NewUUID().String(),
State: enumspb.NAMESPACE_STATE_REGISTERED,
Name: namespace,
},
Config: &persistencespb.NamespaceConfig{
Retention: timestamp.DurationFromHours(24),
HistoryArchivalState: enumspb.ARCHIVAL_STATE_DISABLED,
VisibilityArchivalState: enumspb.ARCHIVAL_STATE_DISABLED,
},
ReplicationConfig: &persistencespb.NamespaceReplicationConfig{
ActiveClusterName: activeClusterName,
Clusters: p.GetOrUseDefaultClusters(activeClusterName, nil),
},
FailoverVersion: common.EmptyVersion,
FailoverNotificationVersion: -1,
}
return &NamespaceConfig{
Detail: &detail,
IsGlobal: global,
}
}
func createNamespaceIfNotExists(db sqlplugin.DB, namespace *NamespaceConfig) error {
var (
name = namespace.Detail.GetInfo().GetName()
id = primitives.MustParseUUID(namespace.Detail.GetInfo().GetId())
)
// Return early if namespace already exists
rows, err := db.SelectFromNamespace(context.Background(), sqlplugin.NamespaceFilter{
Name: &name,
})
if err == nil && len(rows) > 0 {
return nil
}
blob, err := serialization.NewSerializer().NamespaceDetailToBlob(namespace.Detail, enumspb.ENCODING_TYPE_PROTO3)
if err != nil {
return err
}
if _, err := db.InsertIntoNamespace(context.Background(), &sqlplugin.NamespaceRow{
ID: id,
Name: name,
Data: blob.GetData(),
DataEncoding: blob.GetEncodingType().String(),
IsGlobal: namespace.IsGlobal,
NotificationVersion: 0,
}); err != nil {
return err
}
return nil
}