rsmap

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 21, 2023 License: MIT Imports: 25 Imported by: 0

README

rsmap

Go Reference coverage

rsmap is a package for exclusive control between parallelized go test processes.

Why?

When developing applications that rely on a data store, multiple packages may operate on the same data store.

If tests for each package running in parallel or each test case performs data manipulation simultaneously, the stored data may end up in an unintended state, leading to potential test failures.

To avoid this, it is necessary either to run tests for all packages in a single process (go test -p=1) or to implement cross-process mutual exclusion to ensure that operations are exclusive across processes.

The former option increases the time required for test execution. Using dedicated commands/processes to achieve the latter compromises Go's great virtue in portability.

Efficiently executing tests that share a data store while maintaining reliability requires avoiding both of these pitfalls.

How to use

// ./internal/pkg/users_test.go
var userDB *rsmap.Resource

func TestMain(m *testing.M) {
    var err error
    m, err = rsmap.New("../../.rsmap")
    if err != nil {
        log.Panic(err)
    }
    defer m.Close()

    userDB, err = m.Resource(ctx, "user_db") // "user_db" is an identifier for user database
    if err != nil {
        log.Panic(err)
    }

    m.Run()
}

func TestListUsers(t *testing.T) {
    err := userDB.Lock(ctx)
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        _ = userDB.UnlockAny()
    })

    users, err := userRepo.ListUsers(ctx)
    // Test against users.
}
// ./users/create_test.go
var userDB *rsmap.Resource

func TestMain(m *testing.M) {
    var err error
    m, err = rsmap.New("../.rsmap")
    if err != nil {
        log.Panic(err)
    }
    defer m.Close()

    userDB, err = m.Resource(ctx, "user_db")
    if err != nil {
        log.Panic(err)
    }

    m.Run()
}

func TestCreateUser(t *testing.T) {
    err := userDB.Lock(ctx)
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        _ = userDB.UnlockAny()
    })

    users, err := userRepo.CreateUser(ctx, param)
    // Test against user creation.
}

How it works

rsmap.New() creates a database file (BoltDB) within the directory specified as an argument. Since only one process can concurrently open a BoltDB database, the process that initially creates/opens the database has the authority to perform read and write operations.

The instance of rsmap.Map with permissions to manipulate this database launches a server in the background, serving as the interface for exclusive control. Other processes/instances act as clients, requesting the acquisition or release of locks from the server.

Each test process corresponds to a test-specific binary for a package. This means that the process at the core of exclusive control promptly terminates once all tests for its respective package have completed.

Processes that act as clients continue to wait in the background until the database becomes available. This ensures that when the process responsible for the server terminates, another process immediately takes on the role of the server. Subsequently, all other clients start making requests to the new server.

Without the need for dedicated commands or processes, developers can achieve cross-process exclusive control seamlessly.

View rsmap operation log using viewlogs command

I am paying careful attention to enhance the reliability of exclusive control and the switching between server and client roles. The database file, which persists the state of exclusive control, records events that occur during the process.

By examining these events when a test fails due to unexpected reasons, it is possible to aid in identifying the cause. This approach contributes to troubleshooting and understanding the reasons behind unexpected test failures.

$ go run github.com/daichitakahashi/rsmap/cmd/viewlogs YOUR_DATABASE_FILE

viewlogs

Option Short Description
--operation -o Specify the desired information to output, comma-separated, among server (start/stop server), init (initialize resources), acquire (acquire/release locks). By default, it displays all information.
--resource -r Specify the resource for which logs should be output. By default, it outputs logs for all resources.
--short -s Omit the output of log context (location where each function/method was called) and display only the hash.

Documentation

Index

Constants

View Source
const (
	EnvExecutionID = "RSMAP_EXECUTION_ID"
)

Variables

This section is empty.

Functions

func LockResources added in v0.0.4

func LockResources(ctx context.Context, resources ...*ResourceLocker) (func() error, error)

LockResources acquires exclusive/shared locks for multiple resources. Returned function releases all locks acquired.

Types

type InitFunc

type InitFunc func(ctx context.Context) error

type Map

type Map struct {
	// contains filtered or unexported fields
}

Map is the controller for external resource usage.

func New

func New(rsmapDir string, opts ...*NewOption) (*Map, error)

New creates an instance of Map that enables us to reuse external resources with thread safety. Most common use-case is Go's parallelized testing of multiple packages (`go test -p=N ./...`.)

Map has server mode and client mode. If Map has initialized in server mode, it creates database `logs.db` and write server address to `addr` under `${rsmapDir}/${executionID}/`. Other Map reads address of the server, and requests the server to acquire locks. So, every Go packages(directories) must specify same location as an argument. Otherwise, we cannot provide correct control. It's user's responsibility.

`executionID` is the identifier of the execution of `go test`. In default, we use the value of [os.Getppid()]. If you want to specify the id explicitly, set the value to `RSMAP_EXECUTION_ID` environment variable.

In almost cases, following code can be helpful.

p,  _ := exec.Command("go", "mod", "GOMOD").Output() // Get file path of "go.mod".
m, _ := rsmap.New(filepath.Join(filepath.Dir(strings.TrimSpace(string(p))), ".rsmap"))

func (*Map) Close

func (m *Map) Close()

func (*Map) Resource

func (m *Map) Resource(ctx context.Context, name string, opts ...*ResourceOption) (*Resource, error)

Resource creates Resource object that provides control for resource usage.

Resource has a setting for max parallelism, you can specify the value by WithMaxParallelism(default value is 5.) And you want to perform an initialization of the resource, use WithInit.

type NewOption

type NewOption struct {
	option.Interface
}

func WithHTTPClient

func WithHTTPClient(c *http.Client) *NewOption

WithHTTPClient specifies http.Client used for communication with server process. If your process launches server, this client may not be used.

func WithRetryPolicy

func WithRetryPolicy(p backoff.Policy) *NewOption

WithRetryPolicy specifies a retry policy of each operations(resource initializations, lock acquisitions). For example, interval, exponential-backoff and max retry. For detailed settings, see backoff.NewExponentialPolicy or backoff.NewConstantPolicy.

type Resource

type Resource struct {
	// contains filtered or unexported fields
}

Resource brings an ability of acquire/release lock for the dedicated resource.

func (*Resource) Exclusive added in v0.0.4

func (r *Resource) Exclusive() *ResourceLocker

EXclusive returns ResourceLocker for Resource.

func (*Resource) Lock

func (r *Resource) Lock(ctx context.Context) error

Lock acquires exclusive lock of the Resource. The instance of Resource can acquire only one lock. Consecutive acquisition without unlock doesn't fail, but do nothing.

To release lock, use [UnlockAny].

func (*Resource) RLock

func (r *Resource) RLock(ctx context.Context) error

RLock acquires shared lock of the Resource. The instance of Resource can acquire only one lock. Consecutive acquisition without unlock doesn't fail, but do nothing.

To release lock, use [UnlockAny].

func (*Resource) Shared added in v0.0.4

func (r *Resource) Shared() *ResourceLocker

Shared returns ResourceLocker for Resource.

func (*Resource) UnlockAny

func (r *Resource) UnlockAny() error

UnlockAny releases acquired shared/exclusive lock by the Resource.

type ResourceLocker added in v0.0.4

type ResourceLocker struct {
	// contains filtered or unexported fields
}

type ResourceOption

type ResourceOption struct {
	option.Interface
}

ResourceOption represents option for Resource

func WithInit

func WithInit(init InitFunc) *ResourceOption

WithInit specifies InitFunc for resource initialization.

InitFunc will be called only once globally, at first declaration by Resource. Other process waits until the completion of this initialization. So, if Resource is called without this option, we cannot perform initializations with concurrency safety.

func WithMaxParallelism

func WithMaxParallelism(n int64) *ResourceOption

WithMaxParallelism specifies max parallelism of the resource usage.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL