milvus/internal/util/cgo/futures.go
Zhen Ye 47da9023a6
fix: add future stateful lock (#36332)
issue: #36323

Signed-off-by: chyezh <chyezh@outlook.com>
2024-09-18 20:15:11 +08:00

204 lines
5.0 KiB
Go

package cgo
/*
#cgo pkg-config: milvus_core
#include "futures/future_c.h"
#include <stdlib.h>
extern void unlockMutex(void*);
static inline void unlockMutexOnC(CLockedGoMutex* m) {
unlockMutex((void*)(m));
}
static inline void future_go_register_ready_callback(CFuture* f, CLockedGoMutex* m) {
future_register_ready_callback(f, unlockMutexOnC, m);
}
*/
import "C"
import (
"context"
"sync"
"unsafe"
"github.com/cockroachdb/errors"
"github.com/milvus-io/milvus/pkg/util/merr"
)
var ErrConsumed = errors.New("future is already consumed")
// Would put this in futures.go but for the documented issue with
// exports and functions in preamble
// (https://code.google.com/p/go-wiki/wiki/cgo#Global_functions)
//
//export unlockMutex
func unlockMutex(p unsafe.Pointer) {
m := (*sync.Mutex)(p)
m.Unlock()
}
type basicFuture interface {
// Context return the context of the future.
Context() context.Context
// BlockUntilReady block until the future is ready or canceled.
// caller can call this method multiple times in different concurrent unit.
BlockUntilReady()
// cancel the future with error.
cancel(error)
}
type Future interface {
basicFuture
// BlockAndLeakyGet block until the future is ready or canceled, and return the leaky result.
// Caller should only call once for BlockAndLeakyGet, otherwise the ErrConsumed will returned.
// Caller will get the merr.ErrSegcoreCancel or merr.ErrSegcoreTimeout respectively if the future is canceled or timeout.
// Caller will get other error if the underlying cgo function throws, otherwise caller will get result.
// Caller should free the result after used (defined by caller), otherwise the memory of result is leaked.
BlockAndLeakyGet() (unsafe.Pointer, error)
// Release the resource of the future.
// !!! Release is not concurrent safe with other methods.
// It should be called only once after all method of future is returned.
Release()
}
type (
CFuturePtr unsafe.Pointer
CGOAsyncFunction = func() CFuturePtr
)
// Async is a helper function to call a C async function that returns a future.
func Async(ctx context.Context, f CGOAsyncFunction, opts ...Opt) Future {
initCGO()
options := getDefaultOpt()
// apply options.
for _, opt := range opts {
opt(options)
}
// create a future for caller to use.
var cFuturePtr *C.CFuture
getCGOCaller().call(options.name, func() {
cFuturePtr = (*C.CFuture)(f())
})
ctx, cancel := context.WithCancel(ctx)
future := &futureImpl{
closure: f,
ctx: ctx,
ctxCancel: cancel,
future: cFuturePtr,
opts: options,
state: newFutureState(),
}
// register the future to do timeout notification.
futureManager.Register(future)
return future
}
type futureImpl struct {
ctx context.Context
ctxCancel context.CancelFunc
future *C.CFuture
closure CGOAsyncFunction
opts *options
state futureState
}
// Context return the context of the future.
func (f *futureImpl) Context() context.Context {
return f.ctx
}
// BlockUntilReady block until the future is ready or canceled.
func (f *futureImpl) BlockUntilReady() {
f.blockUntilReady()
}
// BlockAndLeakyGet block until the future is ready or canceled, and return the leaky result.
func (f *futureImpl) BlockAndLeakyGet() (unsafe.Pointer, error) {
f.blockUntilReady()
guard := f.state.LockForConsume()
if guard == nil {
return nil, ErrConsumed
}
defer guard.Unlock()
var ptr unsafe.Pointer
var status C.CStatus
getCGOCaller().call("future_leak_and_get", func() {
status = C.future_leak_and_get(f.future, &ptr)
})
err := ConsumeCStatusIntoError(&status)
if errors.Is(err, merr.ErrSegcoreFollyCancel) {
// mark the error with context error.
return nil, errors.Mark(err, f.ctx.Err())
}
return ptr, err
}
// Release the resource of the future.
func (f *futureImpl) Release() {
// block until ready to release the future.
f.blockUntilReady()
guard := f.state.LockForRelease()
if guard == nil {
return
}
defer guard.Unlock()
// release the future.
getCGOCaller().call("future_destroy", func() {
C.future_destroy(f.future)
})
}
// cancel the future with error.
func (f *futureImpl) cancel(err error) {
// only unready future can be canceled.
guard := f.state.LockForCancel()
if guard == nil {
return
}
defer guard.Unlock()
if errors.IsAny(err, context.DeadlineExceeded, context.Canceled) {
getCGOCaller().call("future_cancel", func() {
C.future_cancel(f.future)
})
return
}
panic("unreachable: invalid cancel error type")
}
// blockUntilReady block until the future is ready or canceled.
func (f *futureImpl) blockUntilReady() {
if !f.state.CheckUnready() {
// only unready future should be block until ready.
return
}
mu := &sync.Mutex{}
mu.Lock()
getCGOCaller().call("future_go_register_ready_callback", func() {
C.future_go_register_ready_callback(f.future, (*C.CLockedGoMutex)(unsafe.Pointer(mu)))
})
mu.Lock()
// mark the future as ready at go side to avoid more cgo calls.
f.state.IntoReady()
// notify the future manager that the future is ready.
f.ctxCancel()
}