Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.

Commit 7b3f816

Browse files
feat: use embedded postgres on Windows (#851)
1 parent a3fe3a5 commit 7b3f816

File tree

12 files changed

+291
-39
lines changed

12 files changed

+291
-39
lines changed

.trunk/configs/cspell.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
"dsname",
4545
"dspc",
4646
"dynamicmap",
47+
"embeddedpostgres",
4748
"embedder",
4849
"embedders",
4950
"envfiles",
5051
"estree",
5152
"euclidian",
5253
"expfmt",
5354
"fatih",
55+
"fergusstrange",
5456
"fkey",
5557
"fnptr",
5658
"fptr",
@@ -84,6 +86,7 @@
8486
"idof",
8587
"iface",
8688
"Infof",
89+
"iofs",
8790
"isatty",
8891
"isize",
8992
"jackc",

.vscode/launch.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"env": {
1111
"FORCE_COLOR": "1",
1212
"MODUS_ENV": "dev",
13-
"MODUS_DEBUG": "true",
14-
"MODUS_DB": "postgresql://postgres:postgres@localhost:5433/my-runtime-db?sslmode=disable" // checkov:skip=CKV_SECRET_4
13+
"MODUS_DEBUG": "true"
1514
},
1615
"args": [
1716
"--refresh=1s",
@@ -28,8 +27,7 @@
2827
"env": {
2928
"FORCE_COLOR": "1",
3029
"MODUS_ENV": "dev",
31-
"MODUS_DEBUG": "true",
32-
"MODUS_DB": "postgresql://postgres:postgres@localhost:5433/my-runtime-db?sslmode=disable" // checkov:skip=CKV_SECRET_4
30+
"MODUS_DEBUG": "true"
3331
},
3432
"args": ["--refresh=1s", "--appPath", "${input:appPath}"]
3533
},
@@ -46,8 +44,7 @@
4644
"MODUS_DEBUG": "true",
4745
"AWS_REGION": "${input:awsRegion}",
4846
"AWS_PROFILE": "${input:awsProfile}",
49-
"AWS_SDK_LOAD_CONFIG": "true",
50-
"MODUS_DB": "postgresql://postgres:postgres@localhost:5433/my-runtime-db?sslmode=disable" // checkov:skip=CKV_SECRET_4
47+
"AWS_SDK_LOAD_CONFIG": "true"
5148
},
5249
"args": [
5350
"--useAwsStorage",

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## UNRELEASED
66

77
- fix: omit parallel_tool_calls in Go OpenAI SDK if it is set to true [#849](https://github.com/hypermodeinc/modus/pull/849)
8+
- feat: use embedded postgres on Windows [#851](https://github.com/hypermodeinc/modus/pull/851)
89

910
## 2025-05-19 - Go SDK 0.18.0-alpha.2
1011

runtime/db/db.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func Stop(ctx context.Context) {
5151
close(globalRuntimePostgresWriter.quit)
5252
<-globalRuntimePostgresWriter.done
5353
pool.Close()
54+
55+
if _embeddedPostgresDB != nil {
56+
shutdownEmbeddedPostgresDB(ctx)
57+
}
5458
}
5559

5660
func logDbWarningOrError(ctx context.Context, err error, msg string) {
@@ -393,9 +397,20 @@ func QueryCollectionVectorsFromCheckpoint(ctx context.Context, collectionName, s
393397
}
394398

395399
func Initialize(ctx context.Context) {
400+
if useModusDB() {
401+
return
402+
}
403+
404+
if useEmbeddedPostgres() {
405+
if err := prepareEmbeddedPostgresDB(ctx); err != nil {
406+
logger.Fatal(ctx).Err(err).Msg("Failed to prepare embedded Postgres database.")
407+
return
408+
}
409+
}
410+
396411
// this will initialize the pool and start the worker
397412
_, err := globalRuntimePostgresWriter.GetPool(ctx)
398-
if err != nil && !useModusDB() {
413+
if err != nil {
399414
logger.Warn(ctx).Err(err).Msg("Metadata database is not available.")
400415
}
401416
go globalRuntimePostgresWriter.worker(ctx)
@@ -437,7 +452,6 @@ var _useModusDB bool
437452

438453
func useModusDB() bool {
439454
_useModusDBOnce.Do(func() {
440-
// this gives us a way to force the use or disuse of ModusDB for development
441455
s := os.Getenv("MODUS_DB_USE_MODUSDB")
442456
if s != "" {
443457
if value, err := strconv.ParseBool(s); err == nil {
@@ -446,8 +460,11 @@ func useModusDB() bool {
446460
}
447461
}
448462

449-
// otherwise, it's based on the environment
450-
_useModusDB = app.IsDevEnvironment()
463+
if app.IsDevEnvironment() {
464+
_useModusDB = !useEmbeddedPostgres()
465+
} else {
466+
_useModusDB = false
467+
}
451468
})
452469
return _useModusDB
453470
}

runtime/db/embeddedpg.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2025 Hypermode Inc.
3+
* Licensed under the terms of the Apache License, Version 2.0
4+
* See the LICENSE file that accompanied this code for further details.
5+
*
6+
* SPDX-FileCopyrightText: 2025 Hypermode Inc. <hello@hypermode.com>
7+
* SPDX-License-Identifier: Apache-2.0
8+
*/
9+
10+
package db
11+
12+
import (
13+
"bufio"
14+
"context"
15+
"embed"
16+
"fmt"
17+
"net"
18+
"os"
19+
"os/exec"
20+
"path/filepath"
21+
"runtime"
22+
"strconv"
23+
24+
"github.com/fatih/color"
25+
"github.com/hypermodeinc/modus/runtime/app"
26+
"github.com/hypermodeinc/modus/runtime/logger"
27+
"github.com/rs/zerolog"
28+
29+
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
30+
"github.com/golang-migrate/migrate/v4"
31+
_ "github.com/golang-migrate/migrate/v4/database/postgres"
32+
"github.com/golang-migrate/migrate/v4/source/iofs"
33+
_ "github.com/golang-migrate/migrate/v4/source/iofs"
34+
)
35+
36+
//go:embed migrations/*.sql
37+
var migrationsFS embed.FS
38+
39+
func useEmbeddedPostgres() bool {
40+
s := os.Getenv("MODUS_DB_USE_EMBEDDED_POSTGRES")
41+
if s != "" {
42+
if value, err := strconv.ParseBool(s); err == nil {
43+
return value
44+
}
45+
}
46+
return runtime.GOOS == "windows"
47+
}
48+
49+
var _embeddedPostgresDB *embeddedpostgres.EmbeddedPostgres
50+
51+
func getEmbeddedPostgresDataDir(ctx context.Context) string {
52+
var dataDir string
53+
appPath := app.Config().AppPath()
54+
if filepath.Base(appPath) == "build" {
55+
// this keeps the data directory outside of the build directory
56+
dataDir = filepath.Join(appPath, "..", ".postgres")
57+
addToGitIgnore(ctx, filepath.Dir(appPath), ".postgres/")
58+
} else {
59+
dataDir = filepath.Join(appPath, ".postgres")
60+
}
61+
return dataDir
62+
}
63+
64+
func prepareEmbeddedPostgresDB(ctx context.Context) error {
65+
66+
dataDir := getEmbeddedPostgresDataDir(ctx)
67+
68+
if err := shutdownPreviousEmbeddedPostgresDB(ctx, dataDir); err != nil {
69+
return fmt.Errorf("error shutting down previous embedded postgres instance: %w", err)
70+
}
71+
72+
port, err := findAvailablePort(5432, 5499)
73+
if err != nil {
74+
return err
75+
}
76+
77+
logger.Info(ctx).Msg("Preparing embedded PostgreSQL database. The db instance will log its output next:")
78+
79+
// Note: releases come from here:
80+
// https://github.com/zonkyio/embedded-postgres-binaries/releases
81+
82+
dbname := "modusdb"
83+
cfg := embeddedpostgres.DefaultConfig().
84+
Port(port).
85+
Database(dbname).
86+
DataPath(dataDir).
87+
Version("17.4.0").
88+
OwnProcessGroup(true).
89+
BinariesPath(getEmbeddedPostgresBinariesPath())
90+
91+
db := embeddedpostgres.NewDatabase(cfg)
92+
if err := db.Start(); err != nil {
93+
return fmt.Errorf("failed to start embedded postgres: %w", err)
94+
}
95+
96+
cs := fmt.Sprintf("postgres://postgres:postgres@localhost:%d/%s?sslmode=disable", port, dbname)
97+
98+
d, err := iofs.New(migrationsFS, "migrations")
99+
if err != nil {
100+
return fmt.Errorf("error creating iofs source: %w", err)
101+
}
102+
103+
m, err := migrate.NewWithSourceInstance("iofs", d, cs)
104+
if err != nil {
105+
return fmt.Errorf("error creating db migrate instance: %w", err)
106+
}
107+
m.Log = &migrateLogger{
108+
logger: logger.Get(ctx),
109+
}
110+
111+
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
112+
return fmt.Errorf("error running db migrations: %w", err)
113+
}
114+
115+
_embeddedPostgresDB = db
116+
os.Setenv("MODUS_DB", cs)
117+
118+
logger.Info(ctx).Msg("Embedded PostgreSQL database started successfully.")
119+
return nil
120+
}
121+
122+
func getEmbeddedPostgresBinariesPath() string {
123+
return filepath.Join(app.ModusHomeDir(), "postgres")
124+
}
125+
126+
func findAvailablePort(startPort, maxPort uint32) (uint32, error) {
127+
for port := startPort; port <= maxPort; port++ {
128+
addr := fmt.Sprintf("localhost:%d", port)
129+
listener, err := net.Listen("tcp", addr)
130+
if err == nil {
131+
listener.Close()
132+
return port, nil
133+
}
134+
}
135+
return 0, fmt.Errorf("no available db ports between %d and %d", startPort, maxPort)
136+
}
137+
138+
func shutdownEmbeddedPostgresDB(ctx context.Context) {
139+
logger.Info(ctx).Msg("Shutting down embedded PostgreSQL database server:")
140+
141+
if err := _embeddedPostgresDB.Stop(); err != nil {
142+
logger.Error(ctx).Err(err).Msg("Failed to stop embedded PostgreSQL database.")
143+
}
144+
}
145+
146+
func shutdownPreviousEmbeddedPostgresDB(ctx context.Context, dataDir string) error {
147+
148+
// does dataDir exist?
149+
if _, err := os.Stat(dataDir); err != nil {
150+
if os.IsNotExist(err) {
151+
return nil
152+
}
153+
return fmt.Errorf("error checking dataDir: %w", err)
154+
}
155+
156+
// does `postmaster.pid` exist?
157+
pidFile := filepath.Join(dataDir, "postmaster.pid")
158+
if _, err := os.Stat(pidFile); err != nil {
159+
if os.IsNotExist(err) {
160+
return nil
161+
}
162+
}
163+
164+
logger.Warn(ctx).Msg("Previous embedded PostgreSQL instance was not shut down cleanly. Shutting it down now:")
165+
166+
// first try to shutdown the process cleanly
167+
pgctl := filepath.Join(getEmbeddedPostgresBinariesPath(), "bin", "pg_ctl")
168+
if runtime.GOOS == "windows" {
169+
pgctl += ".exe"
170+
}
171+
if _, err := os.Stat(pgctl); err == nil {
172+
p := exec.Command(pgctl, "stop", "-w", "-D", dataDir)
173+
p.Stdout = os.Stdout
174+
p.Stderr = os.Stderr
175+
if err := p.Run(); err == nil {
176+
return nil
177+
} else {
178+
logger.Err(ctx, err).Msg("Failed to stop embedded PostgreSQL instance cleanly. Killing db process.")
179+
}
180+
}
181+
182+
// read the pid from the first line of the file
183+
file, err := os.Open(pidFile)
184+
if err != nil {
185+
return fmt.Errorf("error opening pid file: %w", err)
186+
}
187+
defer file.Close()
188+
scanner := bufio.NewScanner(file)
189+
if scanner.Scan() {
190+
pidStr := scanner.Text()
191+
pid, err := strconv.Atoi(pidStr)
192+
if err != nil {
193+
return fmt.Errorf("error parsing pid: %w", err)
194+
}
195+
196+
// kill the process
197+
process, err := os.FindProcess(pid)
198+
if err != nil {
199+
return fmt.Errorf("error finding process: %w", err)
200+
}
201+
if err := process.Kill(); err != nil {
202+
return fmt.Errorf("error killing process: %w", err)
203+
}
204+
}
205+
206+
return nil
207+
}
208+
209+
var migrateLoggerColor = color.New(color.FgWhite, color.Faint)
210+
211+
type migrateLogger struct {
212+
logger *zerolog.Logger
213+
started bool
214+
}
215+
216+
func (l *migrateLogger) Printf(format string, v ...interface{}) {
217+
if !l.started {
218+
l.logger.Info().Msg("Applying db migrations:")
219+
l.started = true
220+
}
221+
migrateLoggerColor.Fprintf(os.Stderr, " "+format, v...)
222+
}
223+
224+
func (l *migrateLogger) Verbose() bool {
225+
return false
226+
}

0 commit comments

Comments
 (0)