@@ -18,7 +18,6 @@ import (
1818 "google.golang.org/grpc/status"
1919 "google.golang.org/protobuf/proto"
2020 "google.golang.org/protobuf/types/known/structpb"
21- "google.golang.org/protobuf/types/known/timestamppb"
2221)
2322
2423// validateAttachedFunctionMatchesRequest validates that an existing attached function's parameters match the request parameters.
@@ -88,10 +87,8 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
8887 }
8988
9089 var attachedFunctionID uuid.UUID = uuid .New ()
91- var nextRun time.Time
92- var skipPhase2 bool // Flag to skip Phase 2 if task is already fully created
9390
94- // ===== Phase 1: Create attached function (if needed) =====
91+ // ===== Step 1: Create attached function with is_ready = false =====
9592 err := s .catalog .txImpl .Transaction (ctx , func (txCtx context.Context ) error {
9693 // Double-check attached function doesn't exist (race condition protection)
9794 concurrentAttachedFunction , err := s .catalog .metaDomain .AttachedFunctionDb (txCtx ).GetByName (req .InputCollectionId , req .Name )
@@ -109,7 +106,7 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
109106 return err
110107 }
111108
112- // Validation passed, reuse the concurrent attached function's data
109+ // Validation passed, reuse the concurrent attached function ID (idempotent)
113110 attachedFunctionID = concurrentAttachedFunction .ID
114111 // Already created, skip Phase 2
115112 skipPhase2 = true
@@ -137,7 +134,6 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
137134 log .Error ("AttachFunction: function not found" , zap .String ("function_name" , req .FunctionName ))
138135 return common .ErrFunctionNotFound
139136 }
140-
141137 // Check if input collection exists
142138 collections , err := s .catalog .metaDomain .CollectionDb (txCtx ).GetCollections ([]string {req .InputCollectionId }, nil , req .TenantId , req .Database , nil , nil , false )
143139 if err != nil {
@@ -191,6 +187,7 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
191187 CreatedAt : now ,
192188 UpdatedAt : now ,
193189 OldestWrittenNonce : nil ,
190+ IsReady : false , // **KEY: Set to false for 3-phase create**
194191 }
195192
196193 err = s .catalog .metaDomain .AttachedFunctionDb (txCtx ).Insert (attachedFunction )
@@ -199,7 +196,7 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
199196 return err
200197 }
201198
202- log .Debug ("AttachFunction: Phase 1: attached function created" ,
199+ log .Debug ("AttachFunction: attached function created with is_ready=false " ,
203200 zap .String ("attached_function_id" , attachedFunctionID .String ()),
204201 zap .String ("name" , req .Name ))
205202 return nil
@@ -209,45 +206,6 @@ func (s *Coordinator) AttachFunction(ctx context.Context, req *coordinatorpb.Att
209206 return nil , err
210207 }
211208
212- // If function is already created, return immediately (idempotency)
213- if skipPhase2 {
214- log .Info ("AttachFunction: function already created, skipping Phase 2" ,
215- zap .String ("attached_function_id" , attachedFunctionID .String ()))
216- return & coordinatorpb.AttachFunctionResponse {
217- Id : attachedFunctionID .String (),
218- }, nil
219- }
220-
221- // ===== Phase 2: Push initial schedule =====
222- log .Debug ("AttachFunction: Phase 2: pushing initial schedule" ,
223- zap .String ("attached_function_id" , attachedFunctionID .String ()))
224- // Push initial schedule to heap service if enabled
225- if s .heapClient == nil {
226- return nil , common .ErrHeapServiceNotEnabled
227- }
228-
229- // Create schedule for the attached function
230- schedule := & coordinatorpb.Schedule {
231- Triggerable : & coordinatorpb.Triggerable {
232- PartitioningUuid : req .InputCollectionId ,
233- SchedulingUuid : attachedFunctionID .String (),
234- },
235- NextScheduled : timestamppb .New (nextRun ),
236- }
237-
238- err = s .heapClient .Push (ctx , req .InputCollectionId , []* coordinatorpb.Schedule {schedule })
239- if err != nil {
240- log .Error ("AttachFunction: Phase 2: failed to push schedule to heap service" ,
241- zap .Error (err ),
242- zap .String ("attached_function_id" , attachedFunctionID .String ()),
243- zap .String ("collection_id" , req .InputCollectionId ))
244- return nil , err
245- }
246-
247- log .Debug ("AttachFunction: Phase 2: pushed schedule to heap service" ,
248- zap .String ("attached_function_id" , attachedFunctionID .String ()),
249- zap .String ("collection_id" , req .InputCollectionId ))
250-
251209 return & coordinatorpb.AttachFunctionResponse {
252210 Id : attachedFunctionID .String (),
253211 }, nil
@@ -287,6 +245,7 @@ func attachedFunctionToProto(attachedFunction *dbmodel.AttachedFunction, functio
287245 DatabaseId : attachedFunction .DatabaseID ,
288246 CreatedAt : uint64 (attachedFunction .CreatedAt .UnixMicro ()),
289247 UpdatedAt : uint64 (attachedFunction .UpdatedAt .UnixMicro ()),
248+ IsReady : attachedFunction .IsReady ,
290249 }
291250 if attachedFunction .OutputCollectionID != nil {
292251 attachedFunctionProto .OutputCollectionId = attachedFunction .OutputCollectionID
@@ -563,7 +522,6 @@ func (s *Coordinator) DetachFunction(ctx context.Context, req *coordinatorpb.Det
563522 // First get the attached function to check if we need to delete the output collection
564523 attachedFunction , err := s .catalog .metaDomain .AttachedFunctionDb (ctx ).GetByID (attachedFunctionID )
565524 if err != nil {
566- // If attached function is not ready (lowest_live_nonce == NULL), treat it as not found
567525 if errors .Is (err , common .ErrAttachedFunctionNotReady ) {
568526 log .Error ("DetachFunction: attached function not ready (not initialized)" )
569527 return nil , status .Error (codes .NotFound , "attached function not found" )
@@ -654,6 +612,134 @@ func (s *Coordinator) GetFunctions(ctx context.Context, req *coordinatorpb.GetFu
654612 }, nil
655613}
656614
615+ // TODO(tanujnay112): Remove this
616+ func (s * Coordinator ) FinishAttachedFunction (ctx context.Context , req * coordinatorpb.FinishAttachedFunctionRequest ) (* coordinatorpb.FinishAttachedFunctionResponse , error ) {
617+ attachedFunctionID , err := uuid .Parse (req .Id )
618+ if err != nil {
619+ log .Error ("FinishAttachedFunction: invalid attached_function_id" , zap .Error (err ))
620+ return nil , err
621+ }
622+
623+ err = s .catalog .metaDomain .AttachedFunctionDb (ctx ).Finish (attachedFunctionID )
624+ if err != nil {
625+ log .Error ("FinishAttachedFunction: failed to finish attached function" , zap .Error (err ))
626+ return nil , err
627+ }
628+
629+ return & coordinatorpb.FinishAttachedFunctionResponse {}, nil
630+ }
631+
632+ // FinishCreateAttachedFunction creates the output collection and sets is_ready to true in a single transaction
633+ func (s * Coordinator ) FinishCreateAttachedFunction (ctx context.Context , req * coordinatorpb.FinishCreateAttachedFunctionRequest ) (* coordinatorpb.FinishCreateAttachedFunctionResponse , error ) {
634+ attachedFunctionID , err := uuid .Parse (req .Id )
635+ if err != nil {
636+ log .Error ("FinishCreateAttachedFunction: invalid attached_function_id" , zap .Error (err ))
637+ return nil , status .Errorf (codes .InvalidArgument , "invalid attached_function_id: %v" , err )
638+ }
639+
640+ // Execute all operations in a transaction for atomicity
641+ err = s .catalog .txImpl .Transaction (ctx , func (txCtx context.Context ) error {
642+ // 1. Get the attached function to retrieve metadata
643+ attachedFunction , err := s .catalog .metaDomain .AttachedFunctionDb (txCtx ).GetByID (attachedFunctionID )
644+ if err != nil {
645+ log .Error ("FinishCreateAttachedFunction: failed to get attached function" , zap .Error (err ))
646+ return err
647+ }
648+ if attachedFunction == nil {
649+ log .Error ("FinishCreateAttachedFunction: attached function not found" )
650+ return status .Errorf (codes .NotFound , "attached function not found" )
651+ }
652+
653+ // 2. Check if output collection already exists (idempotency)
654+ if attachedFunction .OutputCollectionID != nil && * attachedFunction .OutputCollectionID != "" {
655+ log .Info ("FinishCreateAttachedFunction: output collection already exists, skipping creation" ,
656+ zap .String ("existing_collection_id" , * attachedFunction .OutputCollectionID ))
657+ // Still set is_ready in case it wasn't set before
658+ return s .catalog .metaDomain .AttachedFunctionDb (txCtx ).SetReady (attachedFunctionID )
659+ }
660+
661+ // 3. Look up database by ID to get its name
662+ database , err := s .catalog .metaDomain .DatabaseDb (txCtx ).GetByID (attachedFunction .DatabaseID )
663+ if err != nil {
664+ log .Error ("FinishCreateAttachedFunction: failed to get database" , zap .Error (err ))
665+ return err
666+ }
667+ if database == nil {
668+ log .Error ("FinishCreateAttachedFunction: database not found" , zap .String ("database_id" , attachedFunction .DatabaseID ), zap .String ("tenant_id" , attachedFunction .TenantID ))
669+ return common .ErrDatabaseNotFound
670+ }
671+
672+ // 4. Generate new collection UUID
673+ collectionID := types .NewUniqueID ()
674+
675+ // 5. Create the output collection with segments
676+ dimension := int32 (1 ) // Default dimension for attached function output collections
677+ collection := & model.CreateCollection {
678+ ID : collectionID ,
679+ Name : attachedFunction .OutputCollectionName ,
680+ ConfigurationJsonStr : "{}" , // Empty JSON object for default config
681+ TenantID : attachedFunction .TenantID ,
682+ DatabaseName : database .Name ,
683+ Dimension : & dimension ,
684+ Metadata : nil ,
685+ }
686+
687+ // Create segments for the collection (distributed setup)
688+ segments := []* model.Segment {
689+ {
690+ ID : types .NewUniqueID (),
691+ Type : "urn:chroma:segment/vector/hnsw-distributed" ,
692+ Scope : "VECTOR" ,
693+ CollectionID : collectionID ,
694+ },
695+ {
696+ ID : types .NewUniqueID (),
697+ Type : "urn:chroma:segment/metadata/blockfile" ,
698+ Scope : "METADATA" ,
699+ CollectionID : collectionID ,
700+ },
701+ {
702+ ID : types .NewUniqueID (),
703+ Type : "urn:chroma:segment/record/blockfile" ,
704+ Scope : "RECORD" ,
705+ CollectionID : collectionID ,
706+ },
707+ }
708+
709+ _ , _ , err = s .catalog .CreateCollectionAndSegments (txCtx , collection , segments , 0 )
710+ if err != nil {
711+ log .Error ("FinishCreateAttachedFunction: failed to create collection" , zap .Error (err ))
712+ return err
713+ }
714+
715+ // 6. Update attached function with output_collection_id
716+ collectionIDStr := collectionID .String ()
717+ err = s .catalog .metaDomain .AttachedFunctionDb (txCtx ).UpdateOutputCollectionID (attachedFunctionID , & collectionIDStr )
718+ if err != nil {
719+ log .Error ("FinishCreateAttachedFunction: failed to update output collection ID" , zap .Error (err ))
720+ return err
721+ }
722+
723+ // 7. Set is_ready to true
724+ err = s .catalog .metaDomain .AttachedFunctionDb (txCtx ).SetReady (attachedFunctionID )
725+ if err != nil {
726+ log .Error ("FinishCreateAttachedFunction: failed to set ready" , zap .Error (err ))
727+ return err
728+ }
729+
730+ log .Info ("FinishCreateAttachedFunction: successfully created output collection and set is_ready=true" ,
731+ zap .String ("attached_function_id" , attachedFunctionID .String ()),
732+ zap .String ("output_collection_id" , collectionID .String ()))
733+ return nil
734+ })
735+
736+ if err != nil {
737+ return nil , err
738+ }
739+
740+ return & coordinatorpb.FinishCreateAttachedFunctionResponse {}, nil
741+ }
742+
657743// CleanupExpiredPartialAttachedFunctions finds and soft deletes attached functions that were partially created
658744// (output_collection_id IS NULL) and are older than the specified max age.
659745// This is used to clean up attached functions that got stuck during creation.
0 commit comments