Skip to content

Commit

Permalink
Support dynamic port allocation
Browse files Browse the repository at this point in the history
  • Loading branch information
yelveyakutinovex committed Mar 8, 2024
1 parent 74c945b commit 2d7e3fe
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 20 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ This library aims to require as little configuration as possible, favouring over
| Port | 5432 |
| StartTimeout | 15 Seconds |

Port can be set to 0 for dynamic port allocation.

The *RuntimePath* directory is erased and recreated at each `Start()` and therefore not suitable for persistent data.

If a persistent data location is required, set *DataPath* to a directory outside *RuntimePath*.
Expand Down
7 changes: 1 addition & 6 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package embeddedpostgres

import (
"fmt"
"io"
"os"
"time"
Expand Down Expand Up @@ -52,7 +51,7 @@ func (c Config) Version(version PostgresVersion) Config {
return c
}

// Port sets the runtime port that Postgres can be accessed on.
// Port sets the runtime port that Postgres can be accessed on. Can be set to 0 for dynamic port allocation.
func (c Config) Port(port uint32) Config {
c.port = port
return c
Expand Down Expand Up @@ -137,10 +136,6 @@ func (c Config) BinaryRepositoryURL(binaryRepositoryURL string) Config {
return c
}

func (c Config) GetConnectionURL() string {
return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", c.username, c.password, "localhost", c.port, c.database)
}

// PostgresVersion represents the semantic version used to fetch and run the Postgres process.
type PostgresVersion string

Expand Down
39 changes: 37 additions & 2 deletions embedded_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (

var mu sync.Mutex

const dynamicallyAllocatedPort = 0

// EmbeddedPostgres maintains all configuration and runtime functions for maintaining the lifecycle of one Postgres process.
type EmbeddedPostgres struct {
config Config
Expand Down Expand Up @@ -66,8 +68,17 @@ func (ep *EmbeddedPostgres) Start() error {
return errors.New("server is already started")
}

if err := ensurePortAvailable(ep.config.port); err != nil {
return err
if ep.config.port == dynamicallyAllocatedPort {
port, err := allocatePort()
if err != nil {
return err
}

ep.config.port = port
} else {
if err := ensurePortAvailable(ep.config.port); err != nil {
return err
}
}

logger, err := newSyncedLogger("", ep.config.logger)
Expand Down Expand Up @@ -174,6 +185,17 @@ func (ep *EmbeddedPostgres) cleanDataDirectoryAndInit() error {
return nil
}

func (ep *EmbeddedPostgres) GetConnectionURL() (string, error) {
if ep.config.port == dynamicallyAllocatedPort {
return "", errors.New("server has to have started to get a connection URL with a dynamically allocated port")
}
return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", ep.config.username, ep.config.password, "localhost", ep.config.port, ep.config.database), nil
}

func (ep *EmbeddedPostgres) GetPort() uint32 {
return ep.config.port
}

// Stop will try to stop the Postgres process gracefully returning an error when there were any problems.
func (ep *EmbeddedPostgres) Stop() error {
if !ep.started {
Expand Down Expand Up @@ -247,6 +269,19 @@ func ensurePortAvailable(port uint32) error {
return nil
}

func allocatePort() (uint32, error) {
conn, err := net.Listen("tcp", "localhost:0")
if err != nil {
return 0, fmt.Errorf("unable to dynamically allocate port %w", err)
}
port := uint32(conn.Addr().(*net.TCPAddr).Port)
if err := conn.Close(); err != nil {
return port, fmt.Errorf("unable to free dynamically allocate port %d for postgres to use", port)
}

return port, nil
}

func dataDirIsValid(dataDir string, version PostgresVersion) bool {
pgVersion := filepath.Join(dataDir, "PG_VERSION")

Expand Down
47 changes: 47 additions & 0 deletions embedded_postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -774,3 +774,50 @@ func Test_RunningInParallel(t *testing.T) {

waitGroup.Wait()
}

func TestGetConnectionURL(t *testing.T) {
config := DefaultConfig().Database("mydb").Username("myuser").Password("mypass")
expect := "postgresql://myuser:mypass@localhost:5432/mydb"

database := NewDatabase(config)

got, err := database.GetConnectionURL()
if err != nil {
t.Errorf("error getting connection url %v", err)
}
if got != expect {
t.Errorf("expected \"%s\" got \"%s\"", expect, got)
}
}

func Test_DynamicPortAllocation(t *testing.T) {
defer verifyLeak(t)

database := NewDatabase(DefaultConfig().Port(0))
if err := database.Start(); err != nil {
shutdownDBAndFail(t, err, database)
}

port := database.GetPort()
if port == 0 {
shutdownDBAndFail(t, errors.New("port should have been allocated dynamically by now"), database)
}

dataSourceName := fmt.Sprintf("host=localhost port=%d user=postgres password=postgres dbname=postgres sslmode=disable", port)
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
shutdownDBAndFail(t, err, database)
}

if err = db.Ping(); err != nil {
shutdownDBAndFail(t, err, database)
}

if err := db.Close(); err != nil {
shutdownDBAndFail(t, err, database)
}

if err := database.Stop(); err != nil {
shutdownDBAndFail(t, err, database)
}
}
12 changes: 0 additions & 12 deletions test_config.go

This file was deleted.

0 comments on commit 2d7e3fe

Please sign in to comment.