-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from xataio/add-replication-handler-tests
Add replication handler tests
- Loading branch information
Showing
17 changed files
with
898 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package postgres | ||
|
||
import ( | ||
"errors" | ||
|
||
"github.com/jackc/pgx/v5/pgconn" | ||
) | ||
|
||
var ErrConnTimeout = errors.New("connection timeout") | ||
|
||
func mapError(err error) error { | ||
if pgconn.Timeout(err) { | ||
return ErrConnTimeout | ||
} | ||
|
||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package mocks | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/jackc/pgx/v5/pgconn" | ||
"github.com/xataio/pgstream/internal/postgres" | ||
) | ||
|
||
type Conn struct { | ||
QueryRowFn func(ctx context.Context, query string, args ...any) postgres.Row | ||
ExecFn func(context.Context, string, ...any) (pgconn.CommandTag, error) | ||
CloseFn func(context.Context) error | ||
} | ||
|
||
func (m *Conn) QueryRow(ctx context.Context, query string, args ...any) postgres.Row { | ||
return m.QueryRowFn(ctx, query, args...) | ||
} | ||
|
||
func (m *Conn) Close(ctx context.Context) error { | ||
return m.CloseFn(ctx) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package mocks | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/xataio/pgstream/internal/postgres" | ||
) | ||
|
||
type ReplicationConn struct { | ||
IdentifySystemFn func(ctx context.Context) (postgres.IdentifySystemResult, error) | ||
StartReplicationFn func(ctx context.Context, cfg postgres.ReplicationConfig) error | ||
SendStandbyStatusUpdateFn func(ctx context.Context, lsn uint64) error | ||
ReceiveMessageFn func(ctx context.Context) (*postgres.ReplicationMessage, error) | ||
CloseFn func(ctx context.Context) error | ||
} | ||
|
||
func (m *ReplicationConn) IdentifySystem(ctx context.Context) (postgres.IdentifySystemResult, error) { | ||
return m.IdentifySystemFn(ctx) | ||
} | ||
|
||
func (m *ReplicationConn) StartReplication(ctx context.Context, cfg postgres.ReplicationConfig) error { | ||
return m.StartReplicationFn(ctx, cfg) | ||
} | ||
|
||
func (m *ReplicationConn) SendStandbyStatusUpdate(ctx context.Context, lsn uint64) error { | ||
return m.SendStandbyStatusUpdateFn(ctx, lsn) | ||
} | ||
|
||
func (m *ReplicationConn) ReceiveMessage(ctx context.Context) (*postgres.ReplicationMessage, error) { | ||
return m.ReceiveMessageFn(ctx) | ||
} | ||
|
||
func (m *ReplicationConn) Close(ctx context.Context) error { | ||
return m.CloseFn(ctx) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package postgres | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/jackc/pgx/v5" | ||
) | ||
|
||
type Conn struct { | ||
conn *pgx.Conn | ||
} | ||
|
||
type Row interface { | ||
pgx.Row | ||
} | ||
|
||
func NewConn(ctx context.Context, url string) (*Conn, error) { | ||
pgCfg, err := pgx.ParseConfig(url) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed parsing postgres connection string: %w", mapError(err)) | ||
} | ||
|
||
conn, err := pgx.ConnectConfig(ctx, pgCfg) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to connect to postgres: %w", mapError(err)) | ||
} | ||
|
||
return &Conn{conn: conn}, nil | ||
} | ||
|
||
func (c *Conn) QueryRow(ctx context.Context, query string, args ...any) Row { | ||
return c.conn.QueryRow(ctx, query, args...) | ||
} | ||
|
||
func (c *Conn) Close(ctx context.Context) error { | ||
return mapError(c.conn.Close(ctx)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package postgres | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/jackc/pglogrepl" | ||
"github.com/jackc/pgx/v5" | ||
"github.com/jackc/pgx/v5/pgconn" | ||
"github.com/jackc/pgx/v5/pgproto3" | ||
) | ||
|
||
type ReplicationConn struct { | ||
conn *pgconn.PgConn | ||
} | ||
|
||
type ReplicationConfig struct { | ||
SlotName string | ||
StartPos uint64 | ||
PluginArguments []string | ||
} | ||
|
||
type ReplicationMessage struct { | ||
LSN uint64 | ||
ServerTime time.Time | ||
WALData []byte | ||
ReplyRequested bool | ||
} | ||
|
||
type IdentifySystemResult pglogrepl.IdentifySystemResult | ||
|
||
var ErrUnsupportedCopyDataMessage = errors.New("unsupported copy data message") | ||
|
||
func NewReplicationConn(ctx context.Context, url string) (*ReplicationConn, error) { | ||
pgCfg, err := pgx.ParseConfig(url) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed parsing postgres connection string: %w", err) | ||
} | ||
|
||
pgCfg.RuntimeParams["replication"] = "database" | ||
|
||
conn, err := pgconn.ConnectConfig(context.Background(), &pgCfg.Config) | ||
if err != nil { | ||
return nil, fmt.Errorf("create postgres replication client: %w", mapError(err)) | ||
} | ||
|
||
return &ReplicationConn{ | ||
conn: conn, | ||
}, nil | ||
} | ||
|
||
func (c *ReplicationConn) IdentifySystem(ctx context.Context) (IdentifySystemResult, error) { | ||
res, err := pglogrepl.IdentifySystem(ctx, c.conn) | ||
return IdentifySystemResult(res), mapError(err) | ||
} | ||
|
||
func (c *ReplicationConn) StartReplication(ctx context.Context, cfg ReplicationConfig) error { | ||
return mapError(pglogrepl.StartReplication( | ||
ctx, | ||
c.conn, | ||
cfg.SlotName, | ||
pglogrepl.LSN(cfg.StartPos), | ||
pglogrepl.StartReplicationOptions{PluginArgs: cfg.PluginArguments})) | ||
} | ||
|
||
func (c *ReplicationConn) SendStandbyStatusUpdate(ctx context.Context, lsn uint64) error { | ||
return mapError(pglogrepl.SendStandbyStatusUpdate( | ||
ctx, | ||
c.conn, | ||
pglogrepl.StandbyStatusUpdate{WALWritePosition: pglogrepl.LSN(lsn)}, | ||
)) | ||
} | ||
|
||
func (c *ReplicationConn) ReceiveMessage(ctx context.Context) (*ReplicationMessage, error) { | ||
msg, err := c.conn.ReceiveMessage(ctx) | ||
if err != nil { | ||
return nil, mapError(err) | ||
} | ||
|
||
switch msg := msg.(type) { | ||
case *pgproto3.CopyData: | ||
switch msg.Data[0] { | ||
case pglogrepl.PrimaryKeepaliveMessageByteID: | ||
pka, err := pglogrepl.ParsePrimaryKeepaliveMessage(msg.Data[1:]) | ||
if err != nil { | ||
return nil, fmt.Errorf("parse keep alive: %w", err) | ||
} | ||
return &ReplicationMessage{ | ||
LSN: uint64(pka.ServerWALEnd), | ||
ServerTime: pka.ServerTime, | ||
ReplyRequested: pka.ReplyRequested, | ||
}, nil | ||
case pglogrepl.XLogDataByteID: | ||
xld, err := pglogrepl.ParseXLogData(msg.Data[1:]) | ||
if err != nil { | ||
return nil, fmt.Errorf("parse xlog data: %w", err) | ||
} | ||
|
||
return &ReplicationMessage{ | ||
LSN: uint64(xld.WALStart) + uint64(len(xld.WALData)), | ||
ServerTime: xld.ServerTime, | ||
WALData: xld.WALData, | ||
}, nil | ||
default: | ||
return nil, fmt.Errorf("%v: %w", msg.Data[0], ErrUnsupportedCopyDataMessage) | ||
} | ||
case *pgproto3.NoticeResponse: | ||
return nil, parseErrNoticeResponse(msg) | ||
default: | ||
// unexpected message (WAL error?) | ||
return nil, fmt.Errorf("unexpected message: %#v", msg) | ||
} | ||
} | ||
|
||
func (c *ReplicationConn) Close(ctx context.Context) error { | ||
return mapError(c.conn.Close(ctx)) | ||
} | ||
|
||
type Error struct { | ||
Severity string | ||
Msg string | ||
} | ||
|
||
func (e *Error) Error() string { | ||
return fmt.Sprintf("replication error: %s", e.Msg) | ||
} | ||
|
||
func parseErrNoticeResponse(errMsg *pgproto3.NoticeResponse) error { | ||
return &Error{ | ||
Severity: errMsg.Severity, | ||
Msg: fmt.Sprintf("replication notice response: severity: %s, code: %s, message: %s, detail: %s, schemaName: %s, tableName: %s, columnName: %s", | ||
errMsg.Severity, errMsg.Code, errMsg.Message, errMsg.Detail, errMsg.SchemaName, errMsg.TableName, errMsg.ColumnName), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.