diff --git a/CHANGELOG.md b/CHANGELOG.md index f282fc3..ed9decf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,22 +2,9 @@ ## UNRELEASED -- feat: add readfrom json tag to support reverse edges - [#49](https://github.com/hypermodeinc/modusgraph/pull/49) +- feat: add a Shutdown function to close the active engine -- chore: Refactoring package management [#51](https://github.com/hypermodeinc/modusgraph/pull/51) - -- fix: alter schema on reverse edge after querying schema - [#55](https://github.com/hypermodeinc/modusgraph/pull/55) - -- feat: update interface to engine and namespace - [#57](https://github.com/hypermodeinc/modusgraph/pull/57) - -- chore: Update dgraph dependency [#62](https://github.com/hypermodeinc/modusgraph/pull/62) - -- fix: add context to api functions [#69](https://github.com/hypermodeinc/modusgraph/pull/69) - -## 2025-01-02 - Version 0.1.0 +## 2025-05-21 - Version 0.1.0 Baseline for the changelog. diff --git a/README.md b/README.md index a8b3419..fc96ca4 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,18 @@ Version 2.0. See the [LICENSE](./LICENSE) file for a complete copy of the licens questions about modus licensing, or need an alternate license or other arrangement, please contact us at . +## Windows Users + +modusGraph (and its dependencies) are designed to work on POSIX-compliant operating systems, and are +not guaranteed to work on Windows. + +Tests at the top level folder (`go test .`) on Windows are maintained to pass, but other tests in +subfolders may not work as expected. + +Temporary folders created during tests may not be cleaned up properly on Windows. Users should +periodically clean up these folders. The temporary folders are created in the Windows temp +directory, `C:\Users\\AppData\Local\Temp\modusgraph_test*`. + ## Acknowledgements modusGraph builds heavily upon packages from the open source projects of diff --git a/admin_test.go b/admin_test.go index d8c25ef..4a91486 100644 --- a/admin_test.go +++ b/admin_test.go @@ -23,7 +23,7 @@ func TestDropData(t *testing.T) { }{ { name: "DropDataWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "DropDataWithDgraphURI", @@ -81,7 +81,7 @@ func TestDropAll(t *testing.T) { }{ { name: "DropAllWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "DropAllWithDgraphURI", @@ -159,7 +159,7 @@ func TestCreateSchema(t *testing.T) { }{ { name: "CreateSchemaWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "CreateSchemaWithDgraphURI", diff --git a/client_test.go b/client_test.go index 9b7184f..93ea47c 100644 --- a/client_test.go +++ b/client_test.go @@ -25,7 +25,7 @@ func TestClientPool(t *testing.T) { }{ { name: "ClientPoolWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "ClientPoolWithDgraphURI", @@ -110,8 +110,8 @@ func TestClientPool(t *testing.T) { }) } - // Reset singleton at the end of the test to ensure the next test can start fresh - mg.ResetSingleton() + // Shutdown at the end of the test to ensure the next test can start fresh + mg.Shutdown() } func TestClientPoolStress(t *testing.T) { @@ -122,7 +122,7 @@ func TestClientPoolStress(t *testing.T) { }{ { name: "ClientPoolStressWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "ClientPoolStressWithDgraphURI", @@ -205,6 +205,6 @@ func TestClientPoolStress(t *testing.T) { require.Greater(t, successCount, 0) }) - mg.ResetSingleton() + mg.Shutdown() } } diff --git a/delete_test.go b/delete_test.go index 8212bc3..c204f66 100644 --- a/delete_test.go +++ b/delete_test.go @@ -24,7 +24,7 @@ func TestClientDelete(t *testing.T) { }{ { name: "DeleteWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "DeleteWithDgraphURI", diff --git a/engine.go b/engine.go index 637841d..41c5589 100644 --- a/engine.go +++ b/engine.go @@ -10,9 +10,11 @@ import ( "errors" "fmt" "path" + "runtime" "strconv" "sync" "sync/atomic" + "time" "github.com/dgraph-io/badger/v4" "github.com/dgraph-io/dgo/v240" @@ -34,6 +36,8 @@ import ( var ( // This ensures that we only have one instance of modusDB in this process. singleton atomic.Bool + // activeEngine tracks the current Engine instance for global access + activeEngine *Engine ErrSingletonOnly = errors.New("only one instance of modusDB can exist in a process") ErrEmptyDataDir = errors.New("data directory is required") @@ -41,12 +45,6 @@ var ( ErrNonExistentDB = errors.New("namespace does not exist") ) -// ResetSingleton resets the singleton state for testing purposes. -// This should ONLY be called during testing, typically in cleanup functions. -func ResetSingleton() { - singleton.Store(false) -} - // Engine is an instance of modusDB. // For now, we only support one instance of modusDB per process. type Engine struct { @@ -105,7 +103,8 @@ func NewEngine(conf Config) (*Engine, error) { engine.logger.Error(err, "Failed to reset database") return nil, fmt.Errorf("error resetting db: %w", err) } - + // Store the engine as the active instance + activeEngine = engine x.UpdateHealthStatus(true) engine.db0 = &Namespace{id: 0, engine: engine} @@ -114,6 +113,16 @@ func NewEngine(conf Config) (*Engine, error) { return engine, nil } +// Shutdown closes the active Engine instance and resets the singleton state. +func Shutdown() { + if activeEngine != nil { + activeEngine.Close() + activeEngine = nil + } + // Reset the singleton state so a new engine can be created if needed + singleton.Store(false) +} + func (engine *Engine) GetClient() (*dgo.Dgraph, error) { engine.logger.V(2).Info("Getting Dgraph client from engine") client, err := createDgraphClient(context.Background(), engine.listener) @@ -378,7 +387,7 @@ func (engine *Engine) LoadData(inCtx context.Context, dataDir string) error { return engine.db0.LoadData(inCtx, dataDir) } -// Close closes the modusDB instance. +// Close closes the modusGraph instance. func (engine *Engine) Close() { engine.mutex.Lock() defer engine.mutex.Unlock() @@ -388,13 +397,18 @@ func (engine *Engine) Close() { } if !singleton.CompareAndSwap(true, false) { - panic("modusDB instance was not properly opened") + panic("modusGraph instance was not properly opened") } engine.isOpen.Store(false) x.UpdateHealthStatus(false) posting.Cleanup() worker.State.Dispose() + + if runtime.GOOS == "windows" { + runtime.GC() + time.Sleep(200 * time.Millisecond) + } } func (ns *Engine) reset() error { diff --git a/insert_test.go b/insert_test.go index 4009ddd..557045c 100644 --- a/insert_test.go +++ b/insert_test.go @@ -35,7 +35,7 @@ func TestClientInsert(t *testing.T) { }{ { name: "InsertWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "InsertWithDgraphURI", @@ -95,7 +95,7 @@ func TestClientInsertMultipleEntities(t *testing.T) { }{ { name: "InsertMultipleWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "InsertMultipleWithDgraphURI", @@ -157,7 +157,7 @@ func TestDepthQuery(t *testing.T) { }{ { name: "InsertWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "InsertWithDgraphURI", diff --git a/query_test.go b/query_test.go index 2292f80..660f404 100644 --- a/query_test.go +++ b/query_test.go @@ -26,7 +26,7 @@ func TestClientSimpleGet(t *testing.T) { }{ { name: "GetWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "GetWithDgraphURI", @@ -86,7 +86,7 @@ func TestClientQuery(t *testing.T) { }{ { name: "QueryWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "QueryWithDgraphURI", @@ -268,7 +268,7 @@ func TestVectorSimilaritySearch(t *testing.T) { }{ { name: "VectorSimilaritySearchWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, /* { diff --git a/unit_test/api_test.go b/unit_test/api_test.go index 3147358..8cf7cdb 100644 --- a/unit_test/api_test.go +++ b/unit_test/api_test.go @@ -1102,20 +1102,3 @@ func TestMultiPolygon(t *testing.T) { require.Equal(t, "Jane Doe", geomStruct.Name) require.Equal(t, multiPolygon.Coordinates, geomStruct.MultiArea.Coordinates) } - -func TestUserStore(t *testing.T) { - ctx := context.Background() - engine, err := modusgraph.NewEngine(modusgraph.NewDefaultConfig("./foo")) - require.NoError(t, err) - defer engine.Close() - - user := User{ - Name: "John Doe", - Age: 30, - } - gid, user, err := modusgraph.Create(ctx, engine, user) - require.NoError(t, err) - require.NotZero(t, gid) - require.Equal(t, "John Doe", user.Name) - require.Equal(t, 30, user.Age) -} diff --git a/update_test.go b/update_test.go index be7291b..9e762bb 100644 --- a/update_test.go +++ b/update_test.go @@ -23,7 +23,7 @@ func TestClientUpdate(t *testing.T) { }{ { name: "UpdateWithFileURI", - uri: "file://" + t.TempDir(), + uri: "file://" + GetTempDir(t), }, { name: "UpdateWithDgraphURI", diff --git a/util_test.go b/util_test.go index 958b802..b22b8c8 100644 --- a/util_test.go +++ b/util_test.go @@ -9,8 +9,12 @@ import ( "context" "log" "os" + "path/filepath" + "runtime" "strconv" + "strings" "testing" + "time" "github.com/go-logr/stdr" mg "github.com/hypermodeinc/modusgraph" @@ -45,13 +49,47 @@ func CreateTestClient(t *testing.T, uri string) (mg.Client, func()) { } client.Close() - // Reset the singleton state so the next test can create a new engine - mg.ResetSingleton() + // Properly shutdown the engine and reset the singleton state + mg.Shutdown() } return client, cleanup } +// GetTempDir returns a temporary directory for testing purposes. +// It creates a unique directory for each test and registers a cleanup function to remove it. +// On Windows, it uses the standard temp directory and creates a unique directory for each test. +// On other platforms, it uses the standard toolchain TempDir function. +func GetTempDir(t *testing.T) string { + if runtime.GOOS == "windows" { + baseDir := os.TempDir() + testName := t.Name() + testName = strings.ReplaceAll(testName, "/", "_") + testName = strings.ReplaceAll(testName, "\\", "_") + testName = strings.ReplaceAll(testName, ":", "_") + + tempDir := filepath.Join(baseDir, "modusgraph_test_"+testName) + + err := os.MkdirAll(tempDir, 0755) + if err != nil { + t.Logf("Failed to create temp directory %s: %v, falling back to standard temp dir", tempDir, err) + return os.TempDir() + } + + t.Cleanup(func() { + runtime.GC() + time.Sleep(200 * time.Millisecond) + + if err := os.RemoveAll(tempDir); err != nil { + t.Logf("Warning: failed to remove temp directory %s: %v", tempDir, err) + } + }) + + return tempDir + } + return t.TempDir() +} + // SetupTestEnv configures the environment variables for tests. // This is particularly useful when debugging tests in an IDE. func SetupTestEnv(logLevel int) {