498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
1377 lines
40 KiB
Go
1377 lines
40 KiB
Go
package function
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/samber/lo"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
|
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
|
|
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
|
"github.com/milvus-io/milvus/pkg/v3/mlog"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/bm25"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/conc"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
|
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
|
)
|
|
|
|
var (
|
|
defaultFunctionRunnerManager FunctionRunnerManager = NewFunctionRunnerManager()
|
|
functionRunnerSingleflight conc.Singleflight[struct{}]
|
|
)
|
|
|
|
var errFunctionRunnerEntryRemoved = errors.New("function runner manager entry was removed")
|
|
|
|
// LatestFunctionRunnerVersion tells the manager to use the latest version
|
|
// snapshot already allocated for the collection. It must not be zero because
|
|
// schema version 0 is valid for compatibility paths.
|
|
const LatestFunctionRunnerVersion int32 = -1
|
|
|
|
type functionRunnerState int
|
|
|
|
const (
|
|
functionRunnerStateInitializing functionRunnerState = iota
|
|
functionRunnerStateReady
|
|
functionRunnerStateFailed
|
|
)
|
|
|
|
type FunctionRunnerManager interface {
|
|
// Alloc records that a lifecycle key is using the schema version
|
|
// and asynchronously tries to initialize function runners for that version.
|
|
// The key identifies an independent lifecycle scope; for example, WAL uses
|
|
// "WAL-"+vchannel while delegator uses "DELEGATOR-"+vchannel.
|
|
// Initialization failures are logged and retried by later Alloc, Update, or
|
|
// Materialize calls instead of failing collection recovery.
|
|
Alloc(collectionID int64, key string, schema *schemapb.CollectionSchema) error
|
|
|
|
// Update moves a lifecycle key to a newer schema version and asynchronously
|
|
// initializes any missing function runners required by that version. If the
|
|
// schema no longer has runner-backed functions, Update releases the key.
|
|
// Runners for older versions are kept until no key uses those versions.
|
|
Update(collectionID int64, key string, schema *schemapb.CollectionSchema) error
|
|
|
|
// Release removes one lifecycle key. The collection entry and its runners
|
|
// are closed only after all keys are released.
|
|
Release(collectionID int64, key string)
|
|
|
|
// Materialize fills missing function output fields for an insert request.
|
|
// Passing a nil schema uses the latest version snapshot already allocated by
|
|
// Create, recovery, or update; any foreground runner initialization uses the
|
|
// schema saved in that entry. Passing a non-nil schema can initialize the
|
|
// matching version. Initialization or execution failures are returned to the
|
|
// caller.
|
|
Materialize(ctx context.Context, collectionID int64, schema *schemapb.CollectionSchema, body *msgpb.InsertRequest) (bool, error)
|
|
|
|
// TryMaterialize is used by compatibility paths for old insert messages. It
|
|
// only uses already cached runners for the given schema version, or the
|
|
// latest allocated version when schemaVersion is LatestFunctionRunnerVersion.
|
|
// It returns ok=false when the caller should build short-lived runners instead.
|
|
TryMaterialize(collectionID int64, schemaVersion int32, body *msgpb.InsertRequest) (bool, bool, error)
|
|
|
|
// RunWithRunner runs the callback with the runner that owns the output field.
|
|
// schemaVersion can be LatestFunctionRunnerVersion to use the latest allocated
|
|
// version. The callback is executed synchronously while the manager protects
|
|
// the runner from concurrent close; callers must not retain the runner after
|
|
// the callback.
|
|
RunWithRunner(ctx context.Context, collectionID int64, schemaVersion int32, outputFieldID int64, run func(schemapb.FunctionType, FunctionRunner) error) (bool, error)
|
|
|
|
// RunWithAnalyzer runs the callback with the analyzer service associated with
|
|
// a function input field. schemaVersion can be LatestFunctionRunnerVersion to
|
|
// use the latest allocated version. BM25 runners can serve analyzer requests.
|
|
// The callback is protected from concurrent close and must not retain the analyzer.
|
|
RunWithAnalyzer(ctx context.Context, collectionID int64, schemaVersion int32, fieldID int64, run func(Analyzer) error) (bool, error)
|
|
|
|
// Close releases all cached runners managed by this manager.
|
|
Close()
|
|
}
|
|
|
|
type functionRunnerManager struct {
|
|
mu sync.RWMutex
|
|
entries map[int64]*functionRunnerCollectionEntry
|
|
}
|
|
|
|
// Lock order is functionRunnerManager.mu -> functionRunnerCollectionEntry.mu ->
|
|
// functionRunnerEntry.mu. The manager lock serializes get-or-create with
|
|
// zero-key removal so a newly allocated lifecycle key cannot reuse an entry that
|
|
// is already being removed. Runner Close calls are always done after releasing
|
|
// the manager/collection locks, so slow runner teardown does not block collection
|
|
// map operations.
|
|
type functionRunnerCollectionEntry struct {
|
|
mu sync.RWMutex
|
|
collectionID int64
|
|
keyVersions map[string]int32
|
|
versionRunners map[int32]*functionRunnerVersion
|
|
runners map[string]*functionRunnerEntry
|
|
}
|
|
|
|
type functionRunnerVersion struct {
|
|
signatures []string
|
|
outputFieldIDs []int64
|
|
outputFieldSignatures map[int64]string
|
|
outputFieldTypes map[int64]schemapb.FunctionType
|
|
analyzerFieldSignatures map[int64]string
|
|
}
|
|
|
|
type functionRunnerEntry struct {
|
|
mu sync.RWMutex
|
|
collectionID int64
|
|
signature string
|
|
schema *schemapb.CollectionSchema
|
|
function *schemapb.FunctionSchema
|
|
runner FunctionRunner
|
|
state functionRunnerState
|
|
initID uint64
|
|
closed bool
|
|
}
|
|
|
|
func newFunctionRunnerCollectionEntry(collectionID int64) *functionRunnerCollectionEntry {
|
|
return &functionRunnerCollectionEntry{
|
|
collectionID: collectionID,
|
|
keyVersions: make(map[string]int32),
|
|
versionRunners: make(map[int32]*functionRunnerVersion),
|
|
runners: make(map[string]*functionRunnerEntry),
|
|
}
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) Alloc(key string, schema *schemapb.CollectionSchema) error {
|
|
return e.allocOrUpdate(key, schema, "initialize")
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) Update(key string, schema *schemapb.CollectionSchema) error {
|
|
return e.allocOrUpdate(key, schema, "update")
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) allocOrUpdate(
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
operation string,
|
|
) error {
|
|
schemaVersion := LatestFunctionRunnerVersion
|
|
if schema != nil {
|
|
schemaVersion = schema.GetVersion()
|
|
}
|
|
warnInitFailure := func(err error) {
|
|
mlog.Warn(context.TODO(), "failed to initialize function runners, will retry on next request",
|
|
mlog.String("operation", operation),
|
|
mlog.Int64("collectionID", e.collectionID),
|
|
mlog.String("key", key),
|
|
mlog.Int32("schemaVersion", schemaVersion),
|
|
mlog.Err(err))
|
|
}
|
|
runnerEntries, err := e.ensureVersion(key, schema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(runnerEntries) == 0 {
|
|
return nil
|
|
}
|
|
go func() {
|
|
for _, runnerEntry := range runnerEntries {
|
|
if err := runnerEntry.ensureReady(context.Background()); err != nil {
|
|
if !errors.Is(err, errFunctionRunnerEntryRemoved) {
|
|
warnInitFailure(err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) Release(key string) bool {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if _, ok := e.keyVersions[key]; !ok {
|
|
return false
|
|
}
|
|
delete(e.keyVersions, key)
|
|
return len(e.keyVersions) == 0
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) Close() {
|
|
e.mu.Lock()
|
|
e.keyVersions = make(map[string]int32)
|
|
runnerEntries := e.gcLocked()
|
|
e.mu.Unlock()
|
|
|
|
closeFunctionRunnerEntries(runnerEntries)
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) GC() {
|
|
e.mu.Lock()
|
|
runnerEntries := e.gcLocked()
|
|
e.mu.Unlock()
|
|
|
|
closeFunctionRunnerEntries(runnerEntries)
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) gcLocked() []*functionRunnerEntry {
|
|
if len(e.keyVersions) == 0 {
|
|
runnerEntries := make([]*functionRunnerEntry, 0, len(e.runners))
|
|
for _, runnerEntry := range e.runners {
|
|
runnerEntries = append(runnerEntries, runnerEntry)
|
|
}
|
|
e.keyVersions = make(map[string]int32)
|
|
e.versionRunners = make(map[int32]*functionRunnerVersion)
|
|
e.runners = make(map[string]*functionRunnerEntry)
|
|
return runnerEntries
|
|
}
|
|
|
|
activeVersions := make(map[int32]struct{}, len(e.keyVersions))
|
|
for _, version := range e.keyVersions {
|
|
activeVersions[version] = struct{}{}
|
|
}
|
|
for version := range e.versionRunners {
|
|
if _, ok := activeVersions[version]; !ok {
|
|
delete(e.versionRunners, version)
|
|
}
|
|
}
|
|
|
|
usedSignatures := make(map[string]struct{})
|
|
for _, versionRunners := range e.versionRunners {
|
|
for _, signature := range versionRunners.signatures {
|
|
usedSignatures[signature] = struct{}{}
|
|
}
|
|
}
|
|
|
|
runnerEntries := make([]*functionRunnerEntry, 0)
|
|
for signature, runnerEntry := range e.runners {
|
|
if _, ok := usedSignatures[signature]; ok {
|
|
continue
|
|
}
|
|
delete(e.runners, signature)
|
|
runnerEntries = append(runnerEntries, runnerEntry)
|
|
}
|
|
return runnerEntries
|
|
}
|
|
|
|
func useLatestFunctionRunnerVersion(schemaVersion int32) bool {
|
|
return schemaVersion == LatestFunctionRunnerVersion
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) getVersionRunnerLocked(schemaVersion int32) (*functionRunnerVersion, bool) {
|
|
if !useLatestFunctionRunnerVersion(schemaVersion) {
|
|
versionRunners, ok := e.versionRunners[schemaVersion]
|
|
return versionRunners, ok
|
|
}
|
|
|
|
if len(e.versionRunners) == 0 {
|
|
return nil, false
|
|
}
|
|
var latestVersion int32
|
|
first := true
|
|
for version := range e.versionRunners {
|
|
if first || version > latestVersion {
|
|
latestVersion = version
|
|
first = false
|
|
}
|
|
}
|
|
return e.versionRunners[latestVersion], true
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) getVersionRunnerEntriesLocked(schemaVersion int32) ([]*functionRunnerEntry, []int64, bool) {
|
|
versionRunners, ok := e.getVersionRunnerLocked(schemaVersion)
|
|
if !ok {
|
|
return nil, nil, false
|
|
}
|
|
runnerEntries := make([]*functionRunnerEntry, 0, len(versionRunners.signatures))
|
|
for _, signature := range versionRunners.signatures {
|
|
runnerEntry := e.runners[signature]
|
|
if runnerEntry == nil {
|
|
return nil, nil, false
|
|
}
|
|
runnerEntries = append(runnerEntries, runnerEntry)
|
|
}
|
|
return runnerEntries, append([]int64(nil), versionRunners.outputFieldIDs...), true
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) getVersionRunnerEntries(schemaVersion int32) ([]*functionRunnerEntry, []int64, bool) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
return e.getVersionRunnerEntriesLocked(schemaVersion)
|
|
}
|
|
|
|
func runWithRunnerEntries(
|
|
ctx context.Context,
|
|
runnerEntries []*functionRunnerEntry,
|
|
initRunner bool,
|
|
run func([]FunctionRunner) error,
|
|
) (bool, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if initRunner {
|
|
for _, runnerEntry := range runnerEntries {
|
|
if err := runnerEntry.ensureReady(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hold runner entry read locks while the callback runs to prevent concurrent
|
|
// close. The read locks are shared, so concurrent materialization is still
|
|
// allowed; concrete runners protect their own mutable state.
|
|
runners := make([]FunctionRunner, 0, len(runnerEntries))
|
|
for _, runnerEntry := range runnerEntries {
|
|
runner, unlock, ok, err := runnerEntry.lockRunner(initRunner)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
runners = append(runners, runner)
|
|
defer unlock()
|
|
}
|
|
|
|
return true, run(runners)
|
|
}
|
|
|
|
func newFunctionRunnerEntry(
|
|
collectionID int64,
|
|
signature string,
|
|
schema *schemapb.CollectionSchema,
|
|
fn *schemapb.FunctionSchema,
|
|
) *functionRunnerEntry {
|
|
return &functionRunnerEntry{
|
|
collectionID: collectionID,
|
|
signature: signature,
|
|
schema: proto.Clone(schema).(*schemapb.CollectionSchema),
|
|
function: proto.Clone(fn).(*schemapb.FunctionSchema),
|
|
state: functionRunnerStateFailed,
|
|
}
|
|
}
|
|
|
|
func (e *functionRunnerEntry) IsReady() bool {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
return e.state == functionRunnerStateReady && e.runner != nil
|
|
}
|
|
|
|
func (e *functionRunnerEntry) ensureReady(ctx context.Context) error {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if e.IsReady() {
|
|
return nil
|
|
}
|
|
|
|
e.mu.Lock()
|
|
if e.closed {
|
|
e.mu.Unlock()
|
|
return errFunctionRunnerEntryRemoved
|
|
}
|
|
if e.state == functionRunnerStateReady && e.runner != nil {
|
|
e.mu.Unlock()
|
|
return nil
|
|
}
|
|
if e.state != functionRunnerStateInitializing {
|
|
e.initID++
|
|
e.state = functionRunnerStateInitializing
|
|
}
|
|
initID := e.initID
|
|
e.mu.Unlock()
|
|
|
|
resultCh := functionRunnerSingleflight.DoChan(e.singleflightKey(), func() (struct{}, error) {
|
|
return struct{}{}, e.init(initID)
|
|
})
|
|
select {
|
|
case result := <-resultCh:
|
|
if result.Err != nil {
|
|
return result.Err
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
|
|
if !e.IsReady() {
|
|
return errFunctionRunnerEntryRemoved
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *functionRunnerEntry) run(ctx context.Context, run func(FunctionRunner) error) error {
|
|
if err := e.ensureReady(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
runner, unlock, _, err := e.lockRunner(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unlock()
|
|
|
|
return run(runner)
|
|
}
|
|
|
|
func (e *functionRunnerEntry) lockRunner(initRunner bool) (FunctionRunner, func(), bool, error) {
|
|
e.mu.RLock()
|
|
|
|
if e.closed || e.state != functionRunnerStateReady || e.runner == nil {
|
|
e.mu.RUnlock()
|
|
if initRunner {
|
|
return nil, nil, false, errFunctionRunnerEntryRemoved
|
|
}
|
|
return nil, nil, false, nil
|
|
}
|
|
return e.runner, e.mu.RUnlock, true, nil
|
|
}
|
|
|
|
func (e *functionRunnerEntry) init(initID uint64) error {
|
|
if e.IsReady() {
|
|
return nil
|
|
}
|
|
|
|
runner, err := BuildEmbeddingRunner(e.schema, e.function)
|
|
if err == nil && runner == nil {
|
|
err = errors.New("function runner is nil")
|
|
}
|
|
|
|
e.mu.Lock()
|
|
if e.closed || e.initID != initID || e.state != functionRunnerStateInitializing {
|
|
e.mu.Unlock()
|
|
if runner != nil {
|
|
runner.Close()
|
|
}
|
|
return errFunctionRunnerEntryRemoved
|
|
}
|
|
|
|
var oldRunner FunctionRunner
|
|
if err != nil {
|
|
e.state = functionRunnerStateFailed
|
|
} else {
|
|
oldRunner = e.runner
|
|
e.runner = runner
|
|
e.state = functionRunnerStateReady
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
if oldRunner != nil {
|
|
oldRunner.Close()
|
|
}
|
|
if err != nil && runner != nil {
|
|
runner.Close()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (e *functionRunnerEntry) singleflightKey() string {
|
|
return strconv.FormatInt(e.collectionID, 10) + "/" + e.signature
|
|
}
|
|
|
|
func (e *functionRunnerEntry) Close() {
|
|
e.mu.Lock()
|
|
runner := e.runner
|
|
e.runner = nil
|
|
e.state = functionRunnerStateFailed
|
|
e.closed = true
|
|
e.mu.Unlock()
|
|
|
|
if runner != nil {
|
|
runner.Close()
|
|
}
|
|
}
|
|
|
|
func NewFunctionRunnerManager() FunctionRunnerManager {
|
|
return newFunctionRunnerManager()
|
|
}
|
|
|
|
func newFunctionRunnerManager() *functionRunnerManager {
|
|
return &functionRunnerManager{
|
|
entries: make(map[int64]*functionRunnerCollectionEntry),
|
|
}
|
|
}
|
|
|
|
func (m *functionRunnerManager) Alloc(
|
|
collectionID int64,
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
) error {
|
|
if key == "" {
|
|
return merr.WrapErrFunctionFailedMsg("function runner key is empty")
|
|
}
|
|
if schema == nil {
|
|
return merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
if !HasEmbeddingFunctions(schema) {
|
|
return nil
|
|
}
|
|
return m.getOrCreateEntry(collectionID).Alloc(key, schema)
|
|
}
|
|
|
|
func (m *functionRunnerManager) Update(
|
|
collectionID int64,
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
) error {
|
|
if key == "" {
|
|
return merr.WrapErrFunctionFailedMsg("function runner key is empty")
|
|
}
|
|
if schema == nil {
|
|
return merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
if !HasEmbeddingFunctions(schema) {
|
|
m.Release(collectionID, key)
|
|
return nil
|
|
}
|
|
return m.getOrCreateEntry(collectionID).Update(key, schema)
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) ensureVersion(
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
) ([]*functionRunnerEntry, error) {
|
|
if schema == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
|
|
schemaVersion := schema.GetVersion()
|
|
e.mu.Lock()
|
|
e.updateKeyVersionLocked(key, schemaVersion)
|
|
|
|
versionRunners, ok := e.versionRunners[schemaVersion]
|
|
if ok {
|
|
runnerEntries := make([]*functionRunnerEntry, 0, len(versionRunners.signatures))
|
|
missingEntry := false
|
|
for _, signature := range versionRunners.signatures {
|
|
runnerEntry := e.runners[signature]
|
|
if runnerEntry == nil {
|
|
missingEntry = true
|
|
break
|
|
}
|
|
if !runnerEntry.IsReady() {
|
|
runnerEntries = append(runnerEntries, runnerEntry)
|
|
}
|
|
}
|
|
if !missingEntry {
|
|
closedRunnerEntries := e.gcLocked()
|
|
e.mu.Unlock()
|
|
|
|
closeFunctionRunnerEntries(closedRunnerEntries)
|
|
return runnerEntries, nil
|
|
}
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
functions := embeddingFunctions(schema)
|
|
functionsBySignature := make(map[string]*schemapb.FunctionSchema, len(functions))
|
|
signatures := make([]string, 0, len(functions))
|
|
outputFieldIDs := make([]int64, 0, len(functions))
|
|
outputFieldSignatures := make(map[int64]string)
|
|
outputFieldTypes := make(map[int64]schemapb.FunctionType)
|
|
analyzerFieldSignatures := make(map[int64]string)
|
|
for _, fn := range functions {
|
|
functionOutputFieldIDs, err := functionOutputFieldIDs(schema, fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inputFieldIDs, err := functionInputFieldIDs(schema, fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature, err := embeddingFunctionSignature(schema, fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signatures = append(signatures, signature)
|
|
outputFieldIDs = append(outputFieldIDs, functionOutputFieldIDs...)
|
|
functionsBySignature[signature] = fn
|
|
for _, outputFieldID := range functionOutputFieldIDs {
|
|
outputFieldSignatures[outputFieldID] = signature
|
|
outputFieldTypes[outputFieldID] = fn.GetType()
|
|
}
|
|
if fn.GetType() == schemapb.FunctionType_BM25 && len(inputFieldIDs) > 0 {
|
|
if _, ok := analyzerFieldSignatures[inputFieldIDs[0]]; !ok {
|
|
analyzerFieldSignatures[inputFieldIDs[0]] = signature
|
|
}
|
|
}
|
|
}
|
|
|
|
e.mu.Lock()
|
|
e.updateKeyVersionLocked(key, schemaVersion)
|
|
|
|
versionRunners, ok = e.versionRunners[schemaVersion]
|
|
if !ok {
|
|
versionRunners = &functionRunnerVersion{
|
|
signatures: append([]string(nil), signatures...),
|
|
outputFieldIDs: append([]int64(nil), outputFieldIDs...),
|
|
outputFieldSignatures: cloneMap(outputFieldSignatures),
|
|
outputFieldTypes: cloneMap(outputFieldTypes),
|
|
analyzerFieldSignatures: cloneMap(analyzerFieldSignatures),
|
|
}
|
|
for _, signature := range signatures {
|
|
if e.runners[signature] == nil {
|
|
e.runners[signature] = newFunctionRunnerEntry(
|
|
e.collectionID,
|
|
signature,
|
|
schema,
|
|
functionsBySignature[signature],
|
|
)
|
|
}
|
|
}
|
|
e.versionRunners[schemaVersion] = versionRunners
|
|
} else {
|
|
for _, signature := range versionRunners.signatures {
|
|
if _, functionOK := functionsBySignature[signature]; !functionOK {
|
|
e.mu.Unlock()
|
|
return nil, merr.WrapErrFunctionFailedMsg("function runner signature %s not found in schema version %d", signature, schemaVersion)
|
|
}
|
|
if e.runners[signature] == nil {
|
|
e.runners[signature] = newFunctionRunnerEntry(
|
|
e.collectionID,
|
|
signature,
|
|
schema,
|
|
functionsBySignature[signature],
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
initRunnerEntries := make([]*functionRunnerEntry, 0, len(versionRunners.signatures))
|
|
for _, signature := range versionRunners.signatures {
|
|
runnerEntry := e.runners[signature]
|
|
if runnerEntry == nil || runnerEntry.IsReady() {
|
|
continue
|
|
}
|
|
initRunnerEntries = append(initRunnerEntries, runnerEntry)
|
|
}
|
|
runnerEntries := e.gcLocked()
|
|
e.mu.Unlock()
|
|
|
|
closeFunctionRunnerEntries(runnerEntries)
|
|
return initRunnerEntries, nil
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) updateKeyVersionLocked(
|
|
key string,
|
|
schemaVersion int32,
|
|
) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
|
|
version, ok := e.keyVersions[key]
|
|
if !ok {
|
|
e.keyVersions[key] = schemaVersion
|
|
return
|
|
}
|
|
|
|
if version <= schemaVersion {
|
|
e.keyVersions[key] = schemaVersion
|
|
}
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) getRunnerEntryByOutputField(schemaVersion int32, outputFieldID int64) (*functionRunnerEntry, schemapb.FunctionType, bool) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
versionRunners, ok := e.getVersionRunnerLocked(schemaVersion)
|
|
if !ok {
|
|
return nil, schemapb.FunctionType_Unknown, false
|
|
}
|
|
signature, ok := versionRunners.outputFieldSignatures[outputFieldID]
|
|
if !ok {
|
|
return nil, schemapb.FunctionType_Unknown, false
|
|
}
|
|
runnerEntry := e.runners[signature]
|
|
if runnerEntry == nil {
|
|
return nil, schemapb.FunctionType_Unknown, false
|
|
}
|
|
return runnerEntry, versionRunners.outputFieldTypes[outputFieldID], true
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) getRunnerEntryByAnalyzerField(schemaVersion int32, fieldID int64) (*functionRunnerEntry, bool) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
versionRunners, ok := e.getVersionRunnerLocked(schemaVersion)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
signature, ok := versionRunners.analyzerFieldSignatures[fieldID]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
runnerEntry := e.runners[signature]
|
|
if runnerEntry == nil {
|
|
return nil, false
|
|
}
|
|
return runnerEntry, true
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) Materialize(
|
|
ctx context.Context,
|
|
schema *schemapb.CollectionSchema,
|
|
body *msgpb.InsertRequest,
|
|
) (bool, error) {
|
|
if body == nil {
|
|
return false, merr.WrapErrFunctionFailedMsg("insert request is nil")
|
|
}
|
|
|
|
var runnerEntries []*functionRunnerEntry
|
|
var outputFieldIDs []int64
|
|
var ok bool
|
|
if schema != nil {
|
|
if _, err := e.ensureVersion("", schema); err != nil {
|
|
return false, err
|
|
}
|
|
runnerEntries, outputFieldIDs, ok = e.getVersionRunnerEntries(schema.GetVersion())
|
|
} else {
|
|
runnerEntries, outputFieldIDs, ok = e.getVersionRunnerEntries(LatestFunctionRunnerVersion)
|
|
}
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
if len(outputFieldIDs) == 0 || HasAllFieldDataByID(body.GetFieldsData(), outputFieldIDs) {
|
|
return false, nil
|
|
}
|
|
|
|
changed := false
|
|
ok, err := runWithRunnerEntries(ctx, runnerEntries, true, func(runners []FunctionRunner) error {
|
|
var runErr error
|
|
changed, runErr = FillFunctionFields(runners, body)
|
|
return runErr
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
// TryMaterialize is only used by compatibility logic to try materializing old
|
|
// insert messages with cached runners.
|
|
func (e *functionRunnerCollectionEntry) TryMaterialize(
|
|
schemaVersion int32,
|
|
body *msgpb.InsertRequest,
|
|
) (bool, bool, error) {
|
|
if body == nil {
|
|
return false, false, merr.WrapErrFunctionFailedMsg("insert request is nil")
|
|
}
|
|
|
|
runnerEntries, outputFieldIDs, ok := e.getVersionRunnerEntries(schemaVersion)
|
|
if !ok {
|
|
return false, false, nil
|
|
}
|
|
if len(outputFieldIDs) == 0 || HasAllFieldDataByID(body.GetFieldsData(), outputFieldIDs) {
|
|
return false, true, nil
|
|
}
|
|
|
|
changed := false
|
|
ok, err := runWithRunnerEntries(context.Background(), runnerEntries, false, func(runners []FunctionRunner) error {
|
|
var runErr error
|
|
changed, runErr = FillFunctionFields(runners, body)
|
|
return runErr
|
|
})
|
|
if err != nil {
|
|
return false, true, err
|
|
}
|
|
return changed, ok, nil
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) RunWithRunner(
|
|
ctx context.Context,
|
|
schemaVersion int32,
|
|
outputFieldID int64,
|
|
run func(schemapb.FunctionType, FunctionRunner) error,
|
|
) (bool, error) {
|
|
runnerEntry, functionType, ok := e.getRunnerEntryByOutputField(schemaVersion, outputFieldID)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
|
|
err := runnerEntry.run(ctx, func(runner FunctionRunner) error {
|
|
return run(functionType, runner)
|
|
})
|
|
return true, err
|
|
}
|
|
|
|
func (e *functionRunnerCollectionEntry) RunWithAnalyzer(
|
|
ctx context.Context,
|
|
schemaVersion int32,
|
|
fieldID int64,
|
|
run func(Analyzer) error,
|
|
) (bool, error) {
|
|
runnerEntry, ok := e.getRunnerEntryByAnalyzerField(schemaVersion, fieldID)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
err := runnerEntry.run(ctx, func(runner FunctionRunner) error {
|
|
analyzer, ok := runner.(Analyzer)
|
|
if !ok {
|
|
return merr.WrapErrFunctionFailedMsg("function runner cannot serve analyzer requests")
|
|
}
|
|
return run(analyzer)
|
|
})
|
|
return true, err
|
|
}
|
|
|
|
func (m *functionRunnerManager) getOrCreateEntry(collectionID int64) *functionRunnerCollectionEntry {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
entry := m.entries[collectionID]
|
|
if entry == nil {
|
|
entry = newFunctionRunnerCollectionEntry(collectionID)
|
|
m.entries[collectionID] = entry
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func (m *functionRunnerManager) getEntry(collectionID int64) *functionRunnerCollectionEntry {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
return m.entries[collectionID]
|
|
}
|
|
|
|
func (m *functionRunnerManager) Release(collectionID int64, key string) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
|
|
m.mu.Lock()
|
|
entry := m.entries[collectionID]
|
|
if entry == nil {
|
|
m.mu.Unlock()
|
|
return
|
|
}
|
|
remove := entry.Release(key)
|
|
if remove {
|
|
delete(m.entries, collectionID)
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if remove {
|
|
entry.Close()
|
|
return
|
|
}
|
|
entry.GC()
|
|
}
|
|
|
|
func (m *functionRunnerManager) Materialize(
|
|
ctx context.Context,
|
|
collectionID int64,
|
|
schema *schemapb.CollectionSchema,
|
|
body *msgpb.InsertRequest,
|
|
) (bool, error) {
|
|
if schema != nil && !HasEmbeddingFunctions(schema) {
|
|
return false, nil
|
|
}
|
|
entry := m.getEntry(collectionID)
|
|
if entry == nil {
|
|
if schema == nil {
|
|
return false, nil
|
|
}
|
|
return false, merr.WrapErrFunctionFailedMsg("function runners for collection %d are not allocated", collectionID)
|
|
}
|
|
return entry.Materialize(ctx, schema, body)
|
|
}
|
|
|
|
func (m *functionRunnerManager) TryMaterialize(
|
|
collectionID int64,
|
|
schemaVersion int32,
|
|
body *msgpb.InsertRequest,
|
|
) (bool, bool, error) {
|
|
entry := m.getEntry(collectionID)
|
|
if entry == nil {
|
|
return false, false, nil
|
|
}
|
|
return entry.TryMaterialize(schemaVersion, body)
|
|
}
|
|
|
|
func (m *functionRunnerManager) RunWithRunner(
|
|
ctx context.Context,
|
|
collectionID int64,
|
|
schemaVersion int32,
|
|
outputFieldID int64,
|
|
run func(schemapb.FunctionType, FunctionRunner) error,
|
|
) (bool, error) {
|
|
entry := m.getEntry(collectionID)
|
|
if entry == nil {
|
|
return false, nil
|
|
}
|
|
return entry.RunWithRunner(ctx, schemaVersion, outputFieldID, run)
|
|
}
|
|
|
|
func (m *functionRunnerManager) RunWithAnalyzer(
|
|
ctx context.Context,
|
|
collectionID int64,
|
|
schemaVersion int32,
|
|
fieldID int64,
|
|
run func(Analyzer) error,
|
|
) (bool, error) {
|
|
entry := m.getEntry(collectionID)
|
|
if entry == nil {
|
|
return false, nil
|
|
}
|
|
return entry.RunWithAnalyzer(ctx, schemaVersion, fieldID, run)
|
|
}
|
|
|
|
func (m *functionRunnerManager) Close() {
|
|
m.mu.Lock()
|
|
entries := make([]*functionRunnerCollectionEntry, 0, len(m.entries))
|
|
for collectionID, entry := range m.entries {
|
|
entries = append(entries, entry)
|
|
delete(m.entries, collectionID)
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
for _, entry := range entries {
|
|
entry.Close()
|
|
}
|
|
}
|
|
|
|
func BuildEmbeddingRunner(schema *schemapb.CollectionSchema, fn *schemapb.FunctionSchema) (FunctionRunner, error) {
|
|
if schema == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
if fn == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function schema is nil")
|
|
}
|
|
if !IsEmbeddingFunctionType(fn.GetType()) {
|
|
return nil, nil
|
|
}
|
|
|
|
schema = proto.Clone(schema).(*schemapb.CollectionSchema)
|
|
fn = proto.Clone(fn).(*schemapb.FunctionSchema)
|
|
return NewFunctionRunner(schema, fn)
|
|
}
|
|
|
|
func BuildEmbeddingRunners(schema *schemapb.CollectionSchema) ([]FunctionRunner, error) {
|
|
if schema == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
if !HasEmbeddingFunctions(schema) {
|
|
return nil, nil
|
|
}
|
|
|
|
functions := embeddingFunctions(schema)
|
|
runners := make([]FunctionRunner, 0, len(functions))
|
|
for _, fn := range functions {
|
|
runner, err := BuildEmbeddingRunner(schema, fn)
|
|
if err != nil {
|
|
CloseRunners(runners)
|
|
return nil, err
|
|
}
|
|
if runner != nil {
|
|
runners = append(runners, runner)
|
|
}
|
|
}
|
|
return runners, nil
|
|
}
|
|
|
|
func EmbeddingOutputFieldIDs(schema *schemapb.CollectionSchema) ([]int64, error) {
|
|
if schema == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
if !HasEmbeddingFunctions(schema) {
|
|
return nil, nil
|
|
}
|
|
|
|
functions := embeddingFunctions(schema)
|
|
outputFieldIDs := make([]int64, 0, len(functions))
|
|
for _, fn := range functions {
|
|
functionOutputFieldIDs, err := functionOutputFieldIDs(schema, fn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputFieldIDs = append(outputFieldIDs, functionOutputFieldIDs...)
|
|
}
|
|
return outputFieldIDs, nil
|
|
}
|
|
|
|
func EmbeddingFunctionSignature(schema *schemapb.CollectionSchema) (string, error) {
|
|
if schema == nil {
|
|
return "", merr.WrapErrFunctionFailedMsg("collection schema is nil")
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
for _, fn := range embeddingFunctions(schema) {
|
|
signature, err := embeddingFunctionSignature(schema, fn)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fmt.Fprintf(hasher, "runner:%s|", signature)
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
// HasEmbeddingFunctions reports whether the schema has functions backed by FunctionRunner.
|
|
func HasEmbeddingFunctions(schema *schemapb.CollectionSchema) bool {
|
|
if schema == nil {
|
|
return false
|
|
}
|
|
for _, fn := range schema.GetFunctions() {
|
|
if IsEmbeddingFunctionType(fn.GetType()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func embeddingFunctions(schema *schemapb.CollectionSchema) []*schemapb.FunctionSchema {
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
|
|
functions := lo.Filter(schema.GetFunctions(), func(fn *schemapb.FunctionSchema, _ int) bool {
|
|
return IsEmbeddingFunctionType(fn.GetType())
|
|
})
|
|
sort.Slice(functions, func(i, j int) bool {
|
|
if functions[i].GetId() != functions[j].GetId() {
|
|
return functions[i].GetId() < functions[j].GetId()
|
|
}
|
|
if functions[i].GetName() != functions[j].GetName() {
|
|
return functions[i].GetName() < functions[j].GetName()
|
|
}
|
|
return functions[i].GetType() < functions[j].GetType()
|
|
})
|
|
return functions
|
|
}
|
|
|
|
func functionOutputFieldIDs(schema *schemapb.CollectionSchema, fn *schemapb.FunctionSchema) ([]int64, error) {
|
|
if outputIDs := fn.GetOutputFieldIds(); len(outputIDs) > 0 {
|
|
outputFieldIDs := append([]int64(nil), outputIDs...)
|
|
for _, outputFieldID := range outputFieldIDs {
|
|
if typeutil.GetField(schema, outputFieldID) == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s output field %d not found", fn.GetName(), outputFieldID)
|
|
}
|
|
}
|
|
return outputFieldIDs, nil
|
|
}
|
|
|
|
outputNames := fn.GetOutputFieldNames()
|
|
if len(outputNames) == 0 {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s output fields not found", fn.GetName())
|
|
}
|
|
|
|
outputFieldIDs := make([]int64, 0, len(outputNames))
|
|
for _, outputName := range outputNames {
|
|
field := typeutil.GetFieldByName(schema, outputName)
|
|
if field == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s output field %s not found", fn.GetName(), outputName)
|
|
}
|
|
outputFieldIDs = append(outputFieldIDs, field.GetFieldID())
|
|
}
|
|
return outputFieldIDs, nil
|
|
}
|
|
|
|
func functionInputFieldIDs(schema *schemapb.CollectionSchema, fn *schemapb.FunctionSchema) ([]int64, error) {
|
|
if inputIDs := fn.GetInputFieldIds(); len(inputIDs) > 0 {
|
|
inputFieldIDs := append([]int64(nil), inputIDs...)
|
|
for _, inputFieldID := range inputFieldIDs {
|
|
if typeutil.GetField(schema, inputFieldID) == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s input field %d not found", fn.GetName(), inputFieldID)
|
|
}
|
|
}
|
|
return inputFieldIDs, nil
|
|
}
|
|
|
|
inputNames := fn.GetInputFieldNames()
|
|
if len(inputNames) == 0 {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s input fields not found", fn.GetName())
|
|
}
|
|
|
|
inputFieldIDs := make([]int64, 0, len(inputNames))
|
|
for _, inputName := range inputNames {
|
|
field := typeutil.GetFieldByName(schema, inputName)
|
|
if field == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function %s input field %s not found", fn.GetName(), inputName)
|
|
}
|
|
inputFieldIDs = append(inputFieldIDs, field.GetFieldID())
|
|
}
|
|
return inputFieldIDs, nil
|
|
}
|
|
|
|
func embeddingFunctionSignature(schema *schemapb.CollectionSchema, fn *schemapb.FunctionSchema) (string, error) {
|
|
hasher := sha256.New()
|
|
fmt.Fprintf(hasher, "fn:%d:%d:%s|", fn.GetId(), fn.GetType(), fn.GetName())
|
|
writeInt64s(hasher, "input_ids", fn.GetInputFieldIds())
|
|
writeStrings(hasher, "input_names", fn.GetInputFieldNames())
|
|
writeInt64s(hasher, "output_ids", fn.GetOutputFieldIds())
|
|
writeStrings(hasher, "output_names", fn.GetOutputFieldNames())
|
|
writeKeyValuePairs(hasher, "fn_params", fn.GetParams())
|
|
|
|
for _, fieldID := range fn.GetInputFieldIds() {
|
|
field := typeutil.GetField(schema, fieldID)
|
|
if field == nil {
|
|
return "", merr.WrapErrFunctionFailedMsg("function %s input field %d not found", fn.GetName(), fieldID)
|
|
}
|
|
writeFieldSignature(hasher, "input", field)
|
|
}
|
|
for _, fieldName := range fn.GetInputFieldNames() {
|
|
field := typeutil.GetFieldByName(schema, fieldName)
|
|
if field == nil {
|
|
return "", merr.WrapErrFunctionFailedMsg("function %s input field %s not found", fn.GetName(), fieldName)
|
|
}
|
|
writeFieldSignature(hasher, "input_name", field)
|
|
}
|
|
for _, fieldID := range fn.GetOutputFieldIds() {
|
|
field := typeutil.GetField(schema, fieldID)
|
|
if field == nil {
|
|
return "", merr.WrapErrFunctionFailedMsg("function %s output field %d not found", fn.GetName(), fieldID)
|
|
}
|
|
writeFieldSignature(hasher, "output", field)
|
|
}
|
|
for _, fieldName := range fn.GetOutputFieldNames() {
|
|
field := typeutil.GetFieldByName(schema, fieldName)
|
|
if field == nil {
|
|
return "", merr.WrapErrFunctionFailedMsg("function %s output field %s not found", fn.GetName(), fieldName)
|
|
}
|
|
writeFieldSignature(hasher, "output_name", field)
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
func writeFieldSignature(hasher hashWriter, prefix string, field *schemapb.FieldSchema) {
|
|
fmt.Fprintf(hasher, "%s:%d:%s:%d:%d:%t:%t:%t:%t|",
|
|
prefix,
|
|
field.GetFieldID(),
|
|
field.GetName(),
|
|
field.GetDataType(),
|
|
field.GetElementType(),
|
|
field.GetIsPrimaryKey(),
|
|
field.GetIsPartitionKey(),
|
|
field.GetIsClusteringKey(),
|
|
field.GetIsFunctionOutput())
|
|
writeKeyValuePairs(hasher, prefix+"_type_params", field.GetTypeParams())
|
|
}
|
|
|
|
func writeKeyValuePairs(hasher hashWriter, prefix string, pairs []*commonpb.KeyValuePair) {
|
|
cloned := append([]*commonpb.KeyValuePair(nil), pairs...)
|
|
sort.Slice(cloned, func(i, j int) bool {
|
|
if cloned[i].GetKey() != cloned[j].GetKey() {
|
|
return cloned[i].GetKey() < cloned[j].GetKey()
|
|
}
|
|
return cloned[i].GetValue() < cloned[j].GetValue()
|
|
})
|
|
for _, pair := range cloned {
|
|
fmt.Fprintf(hasher, "%s:%s=%s|", prefix, pair.GetKey(), pair.GetValue())
|
|
}
|
|
}
|
|
|
|
func writeInt64s(hasher hashWriter, prefix string, values []int64) {
|
|
for idx, value := range values {
|
|
fmt.Fprintf(hasher, "%s:%d=%d|", prefix, idx, value)
|
|
}
|
|
}
|
|
|
|
func writeStrings(hasher hashWriter, prefix string, values []string) {
|
|
for idx, value := range values {
|
|
fmt.Fprintf(hasher, "%s:%d=%s|", prefix, idx, value)
|
|
}
|
|
}
|
|
|
|
type hashWriter interface {
|
|
Write([]byte) (int, error)
|
|
}
|
|
|
|
func cloneMap[K comparable, V any](values map[K]V) map[K]V {
|
|
cloned := make(map[K]V, len(values))
|
|
for key, value := range values {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
// FillFunctionData fills function output fields before appending an insert message to WAL.
|
|
// Passing nil schema uses the latest runner snapshot allocated by collection
|
|
// create, recovery, or update.
|
|
func FillFunctionData(ctx context.Context, collectionID int64, schema *schemapb.CollectionSchema, body *msgpb.InsertRequest) (bool, error) {
|
|
return defaultFunctionRunnerManager.Materialize(ctx, collectionID, schema, body)
|
|
}
|
|
|
|
// TryMaterialize is used by compatibility logic to try materializing old insert
|
|
// messages with cache-managed runners. It returns ok=false when a matching cache
|
|
// is absent so the caller can build a short-lived runner for that message.
|
|
func TryMaterialize(collectionID int64, schemaVersion int32, body *msgpb.InsertRequest) (changed bool, ok bool, err error) {
|
|
return defaultFunctionRunnerManager.TryMaterialize(collectionID, schemaVersion, body)
|
|
}
|
|
|
|
func AllocFunctionRunners(
|
|
collectionID int64,
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
) error {
|
|
return defaultFunctionRunnerManager.Alloc(collectionID, key, schema)
|
|
}
|
|
|
|
func UpdateFunctionRunners(
|
|
collectionID int64,
|
|
key string,
|
|
schema *schemapb.CollectionSchema,
|
|
) error {
|
|
return defaultFunctionRunnerManager.Update(collectionID, key, schema)
|
|
}
|
|
|
|
func ReleaseFunctionRunners(collectionID int64, key string) {
|
|
defaultFunctionRunnerManager.Release(collectionID, key)
|
|
}
|
|
|
|
func RunWithRunner(
|
|
ctx context.Context,
|
|
collectionID int64,
|
|
schemaVersion int32,
|
|
outputFieldID int64,
|
|
run func(schemapb.FunctionType, FunctionRunner) error,
|
|
) (bool, error) {
|
|
return defaultFunctionRunnerManager.RunWithRunner(ctx, collectionID, schemaVersion, outputFieldID, run)
|
|
}
|
|
|
|
func RunWithAnalyzer(
|
|
ctx context.Context,
|
|
collectionID int64,
|
|
schemaVersion int32,
|
|
fieldID int64,
|
|
run func(Analyzer) error,
|
|
) (bool, error) {
|
|
return defaultFunctionRunnerManager.RunWithAnalyzer(ctx, collectionID, schemaVersion, fieldID, run)
|
|
}
|
|
|
|
func FillFunctionFields(runners []FunctionRunner, body *msgpb.InsertRequest) (bool, error) {
|
|
if body == nil {
|
|
return false, merr.WrapErrFunctionFailedMsg("insert request is nil")
|
|
}
|
|
|
|
changed := false
|
|
for _, runner := range runners {
|
|
outputFields := runner.GetOutputFields()
|
|
if len(outputFields) != 1 {
|
|
return false, merr.WrapErrFunctionFailedMsg("function should have exactly one output field, got %d", len(outputFields))
|
|
}
|
|
outputField := outputFields[0]
|
|
if HasFieldData(body.GetFieldsData(), outputField.GetFieldID()) {
|
|
continue
|
|
}
|
|
|
|
output, err := RunFunction(runner, body)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
body.FieldsData = append(body.FieldsData, output)
|
|
changed = true
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
func IsEmbeddingFunctionType(functionType schemapb.FunctionType) bool {
|
|
switch functionType {
|
|
case schemapb.FunctionType_BM25, schemapb.FunctionType_MinHash:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func RunFunction(runner FunctionRunner, body *msgpb.InsertRequest) (*schemapb.FieldData, error) {
|
|
inputIDs := lo.Map(runner.GetInputFields(), func(field *schemapb.FieldSchema, _ int) int64 {
|
|
return field.GetFieldID()
|
|
})
|
|
inputData, err := getStringFieldData(body.GetFieldsData(), inputIDs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
output, err := runner.BatchRun(inputData...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(output) == 0 {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function runner returned empty output")
|
|
}
|
|
|
|
outputFields := runner.GetOutputFields()
|
|
if len(outputFields) != 1 {
|
|
return nil, merr.WrapErrFunctionFailedMsg("function should have exactly one output field, got %d", len(outputFields))
|
|
}
|
|
outputField := outputFields[0]
|
|
|
|
switch runner.GetSchema().GetType() {
|
|
case schemapb.FunctionType_BM25:
|
|
sparseArray, ok := output[0].(*schemapb.SparseFloatArray)
|
|
if !ok {
|
|
return nil, merr.WrapErrFunctionFailedMsg("BM25 runner returned non sparse-float-vector output")
|
|
}
|
|
return bm25.BuildSparseFieldData(outputField, sparseArray), nil
|
|
case schemapb.FunctionType_MinHash:
|
|
fieldData, ok := output[0].(*schemapb.FieldData)
|
|
if !ok {
|
|
return nil, merr.WrapErrFunctionFailedMsg("MinHash runner returned non field-data output")
|
|
}
|
|
fieldData.Type = outputField.GetDataType()
|
|
fieldData.FieldName = outputField.GetName()
|
|
fieldData.FieldId = outputField.GetFieldID()
|
|
return fieldData, nil
|
|
default:
|
|
return nil, merr.WrapErrFunctionFailedMsg("unsupported embedding function type %s", runner.GetSchema().GetType().String())
|
|
}
|
|
}
|
|
|
|
func HasAllFieldDataByID(fieldsData []*schemapb.FieldData, fieldIDs []int64) bool {
|
|
for _, fieldID := range fieldIDs {
|
|
if !HasFieldData(fieldsData, fieldID) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func HasFieldData(fieldsData []*schemapb.FieldData, fieldID int64) bool {
|
|
return GetFieldData(fieldsData, fieldID) != nil
|
|
}
|
|
|
|
func GetFieldData(fieldsData []*schemapb.FieldData, fieldID int64) *schemapb.FieldData {
|
|
for _, fieldData := range fieldsData {
|
|
if fieldData.GetFieldId() == fieldID {
|
|
return fieldData
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CloseRunners(runners []FunctionRunner) {
|
|
for _, runner := range runners {
|
|
if runner != nil {
|
|
runner.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func closeFunctionRunnerEntries(entries []*functionRunnerEntry) {
|
|
for _, entry := range entries {
|
|
if entry != nil {
|
|
entry.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func getStringFieldData(fieldsData []*schemapb.FieldData, fieldIDs ...int64) ([]any, error) {
|
|
result := make([]any, 0, len(fieldIDs))
|
|
for _, fieldID := range fieldIDs {
|
|
fieldData := GetFieldData(fieldsData, fieldID)
|
|
if fieldData == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("field %d not found", fieldID)
|
|
}
|
|
stringData := fieldData.GetScalars().GetStringData()
|
|
if stringData == nil {
|
|
return nil, merr.WrapErrFunctionFailedMsg("field %d is not string data", fieldID)
|
|
}
|
|
result = append(result, stringData.GetData())
|
|
}
|
|
return result, nil
|
|
}
|