@@ -29,6 +29,7 @@ import (
2929"strings"
3030"sync"
3131"sync/atomic"
32+ "testing"
3233"time"
3334
3435"github.com/charmbracelet/lipgloss"
@@ -1377,6 +1378,7 @@ func IsLocalURL(ctx context.Context, u *url.URL) (bool, error) {
13771378}
13781379
13791380func shutdownWithTimeout (shutdown func (context.Context )error ,timeout time.Duration )error {
1381+ // nolint:gocritic // The magic number is parameterized.
13801382ctx ,cancel := context .WithTimeout (context .Background (),timeout )
13811383defer cancel ()
13821384return shutdown (ctx )
@@ -2134,50 +2136,83 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
21342136return "" ,nil ,xerrors .New ("The built-in PostgreSQL cannot run as the root user. Create a non-root user and run again!" )
21352137}
21362138
2137- // Ensure a password and port have been generated!
2138- connectionURL ,err := embeddedPostgresURL (cfg )
2139- if err != nil {
2140- return "" ,nil ,err
2141- }
2142- pgPassword ,err := cfg .PostgresPassword ().Read ()
2143- if err != nil {
2144- return "" ,nil ,xerrors .Errorf ("read postgres password: %w" ,err )
2145- }
2146- pgPortRaw ,err := cfg .PostgresPort ().Read ()
2147- if err != nil {
2148- return "" ,nil ,xerrors .Errorf ("read postgres port: %w" ,err )
2149- }
2150- pgPort ,err := strconv .ParseUint (pgPortRaw ,10 ,16 )
2151- if err != nil {
2152- return "" ,nil ,xerrors .Errorf ("parse postgres port: %w" ,err )
2153- }
2154-
21552139cachePath := filepath .Join (cfg .PostgresPath (),"cache" )
21562140if customCacheDir != "" {
21572141cachePath = filepath .Join (customCacheDir ,"postgres" )
21582142}
21592143stdlibLogger := slog .Stdlib (ctx ,logger .Named ("postgres" ),slog .LevelDebug )
2160- ep := embeddedpostgres .NewDatabase (
2161- embeddedpostgres .DefaultConfig ().
2162- Version (embeddedpostgres .V13 ).
2163- BinariesPath (filepath .Join (cfg .PostgresPath (),"bin" )).
2164- // Default BinaryRepositoryURL repo1.maven.org is flaky.
2165- BinaryRepositoryURL ("https://repo.maven.apache.org/maven2" ).
2166- DataPath (filepath .Join (cfg .PostgresPath (),"data" )).
2167- RuntimePath (filepath .Join (cfg .PostgresPath (),"runtime" )).
2168- CachePath (cachePath ).
2169- Username ("coder" ).
2170- Password (pgPassword ).
2171- Database ("coder" ).
2172- Encoding ("UTF8" ).
2173- Port (uint32 (pgPort )).
2174- Logger (stdlibLogger .Writer ()),
2175- )
2176- err = ep .Start ()
2177- if err != nil {
2178- return "" ,nil ,xerrors .Errorf ("Failed to start built-in PostgreSQL. Optionally, specify an external deployment with `--postgres-url`: %w" ,err )
2144+
2145+ // If the port is not defined, an available port will be found dynamically.
2146+ maxAttempts := 1
2147+ _ ,err = cfg .PostgresPort ().Read ()
2148+ retryPortDiscovery := errors .Is (err ,os .ErrNotExist )&& testing .Testing ()
2149+ if retryPortDiscovery {
2150+ // There is no way to tell Postgres to use an ephemeral port, so in order to avoid
2151+ // flaky tests in CI we need to retry EmbeddedPostgres.Start in case of a race
2152+ // condition where the port we quickly listen on and close in embeddedPostgresURL()
2153+ // is not free by the time the embedded postgres starts up. This maximum_should
2154+ // cover most cases where port conflicts occur in CI and cause flaky tests.
2155+ maxAttempts = 3
2156+ }
2157+
2158+ var startErr error
2159+ for attempt := 0 ;attempt < maxAttempts ;attempt ++ {
2160+ // Ensure a password and port have been generated.
2161+ connectionURL ,err := embeddedPostgresURL (cfg )
2162+ if err != nil {
2163+ return "" ,nil ,err
2164+ }
2165+ pgPassword ,err := cfg .PostgresPassword ().Read ()
2166+ if err != nil {
2167+ return "" ,nil ,xerrors .Errorf ("read postgres password: %w" ,err )
2168+ }
2169+ pgPortRaw ,err := cfg .PostgresPort ().Read ()
2170+ if err != nil {
2171+ return "" ,nil ,xerrors .Errorf ("read postgres port: %w" ,err )
2172+ }
2173+ pgPort ,err := strconv .ParseUint (pgPortRaw ,10 ,16 )
2174+ if err != nil {
2175+ return "" ,nil ,xerrors .Errorf ("parse postgres port: %w" ,err )
2176+ }
2177+
2178+ ep := embeddedpostgres .NewDatabase (
2179+ embeddedpostgres .DefaultConfig ().
2180+ Version (embeddedpostgres .V13 ).
2181+ BinariesPath (filepath .Join (cfg .PostgresPath (),"bin" )).
2182+ // Default BinaryRepositoryURL repo1.maven.org is flaky.
2183+ BinaryRepositoryURL ("https://repo.maven.apache.org/maven2" ).
2184+ DataPath (filepath .Join (cfg .PostgresPath (),"data" )).
2185+ RuntimePath (filepath .Join (cfg .PostgresPath (),"runtime" )).
2186+ CachePath (cachePath ).
2187+ Username ("coder" ).
2188+ Password (pgPassword ).
2189+ Database ("coder" ).
2190+ Encoding ("UTF8" ).
2191+ Port (uint32 (pgPort )).
2192+ Logger (stdlibLogger .Writer ()),
2193+ )
2194+
2195+ startErr = ep .Start ()
2196+ if startErr == nil {
2197+ return connectionURL ,ep .Stop ,nil
2198+ }
2199+
2200+ logger .Warn (ctx ,"failed to start embedded postgres" ,
2201+ slog .F ("attempt" ,attempt + 1 ),
2202+ slog .F ("max_attempts" ,maxAttempts ),
2203+ slog .F ("port" ,pgPort ),
2204+ slog .Error (startErr ),
2205+ )
2206+
2207+ if retryPortDiscovery {
2208+ // Since a retry is needed, we wipe the port stored here at the beginning of the loop.
2209+ _ = cfg .PostgresPort ().Delete ()
2210+ }
21792211}
2180- return connectionURL ,ep .Stop ,nil
2212+
2213+ return "" ,nil ,xerrors .Errorf ("failed to start built-in PostgreSQL after %d attempts. " +
2214+ "Optionally, specify an external deployment. See https://coder.com/docs/tutorials/external-database " +
2215+ "for more details: %w" ,maxAttempts ,startErr )
21812216}
21822217
21832218func ConfigureHTTPClient (ctx context.Context ,clientCertFile ,clientKeyFile string ,tlsClientCAFile string ) (context.Context ,* http.Client ,error ) {
@@ -2286,7 +2321,7 @@ func ConnectToPostgres(ctx context.Context, logger slog.Logger, driver string, d
22862321var err error
22872322var sqlDB * sql.DB
22882323dbNeedsClosing := true
2289- // Try to connect for 30 seconds.
2324+ //nolint:gocritic // Try to connect for 30 seconds.
22902325ctx ,cancel := context .WithTimeout (ctx ,30 * time .Second )
22912326defer cancel ()
22922327
@@ -2382,6 +2417,7 @@ func ConnectToPostgres(ctx context.Context, logger slog.Logger, driver string, d
23822417}
23832418
23842419func pingPostgres (ctx context.Context ,db * sql.DB )error {
2420+ // nolint:gocritic // This is a reasonable magic number for a ping timeout.
23852421ctx ,cancel := context .WithTimeout (ctx ,5 * time .Second )
23862422defer cancel ()
23872423return db .PingContext (ctx )