package batching import ( "fmt" "sync" "time" ) // GitOp represents a git operation to be batched type GitOp struct { OpType string // "commit", "push", "merge" Branch string Message string Files []string Timestamp time.Time ID string } // GitBatch represents a batch of git operations type GitBatch struct { ID string Operations []*GitOp CreatedAt time.Time ExecutedAt time.Time Status string // "pending", "executing", "completed", "failed" Error error } // GitBatcher batches git operations for efficient execution type GitBatcher struct { mu sync.RWMutex queue []*GitOp maxBatchSize int maxBatchAge time.Duration lastFlushTime time.Time executedBatches []*GitBatch pendingBatches []*GitBatch stats *BatchStats flushChan chan struct{} stopChan chan struct{} } // BatchStats tracks batching statistics type BatchStats struct { TotalOps int TotalBatches int AvgOpsPerBatch float64 NetworkSavings int // Estimated network round trips saved TotalExecuteTime time.Duration } // NewGitBatcher creates a new git batcher func NewGitBatcher(maxBatchSize int, maxBatchAge time.Duration) *GitBatcher { if maxBatchSize <= 0 { maxBatchSize = 10 } if maxBatchAge <= 0 { maxBatchAge = 5 * time.Second } return &GitBatcher{ queue: make([]*GitOp, 0), maxBatchSize: maxBatchSize, maxBatchAge: maxBatchAge, lastFlushTime: time.Now(), executedBatches: make([]*GitBatch, 0), pendingBatches: make([]*GitBatch, 0), stats: &BatchStats{ TotalOps: 0, TotalBatches: 0, }, flushChan: make(chan struct{}, 1), stopChan: make(chan struct{}), } } // Enqueue adds a git operation to the queue func (gb *GitBatcher) Enqueue(op *GitOp) { if op == nil { return } op.Timestamp = time.Now() gb.mu.Lock() defer gb.mu.Unlock() gb.queue = append(gb.queue, op) gb.stats.TotalOps++ // Auto-flush if batch is full if len(gb.queue) >= gb.maxBatchSize { gb.flushLocked() } } // flushLocked creates a batch from queued operations (must be called with lock held) func (gb *GitBatcher) flushLocked() { if len(gb.queue) == 0 { return } batch := &GitBatch{ ID: fmt.Sprintf("batch-%d", gb.stats.TotalBatches), Operations: make([]*GitOp, len(gb.queue)), CreatedAt: time.Now(), Status: "pending", } copy(batch.Operations, gb.queue) gb.pendingBatches = append(gb.pendingBatches, batch) gb.queue = make([]*GitOp, 0) gb.lastFlushTime = time.Now() gb.stats.TotalBatches++ } // Flush manually flushes the current batch func (gb *GitBatcher) Flush() { gb.mu.Lock() defer gb.mu.Unlock() gb.flushLocked() } // GetPendingBatch returns the next pending batch without removing it func (gb *GitBatcher) GetPendingBatch() *GitBatch { gb.mu.RLock() defer gb.mu.RUnlock() if len(gb.pendingBatches) == 0 { return nil } return gb.pendingBatches[0] } // MarkBatchExecuting marks a batch as executing func (gb *GitBatcher) MarkBatchExecuting(batchID string) { gb.mu.Lock() defer gb.mu.Unlock() for _, batch := range gb.pendingBatches { if batch.ID == batchID { batch.Status = "executing" break } } } // MarkBatchCompleted marks a batch as completed and removes from pending func (gb *GitBatcher) MarkBatchCompleted(batchID string) { gb.mu.Lock() defer gb.mu.Unlock() var idx int var found *GitBatch for i, batch := range gb.pendingBatches { if batch.ID == batchID { idx = i found = batch break } } if found != nil { found.Status = "completed" found.ExecutedAt = time.Now() // Move to executed batches gb.executedBatches = append(gb.executedBatches, found) // Remove from pending gb.pendingBatches = append(gb.pendingBatches[:idx], gb.pendingBatches[idx+1:]...) } } // MarkBatchFailed marks a batch as failed with an error func (gb *GitBatcher) MarkBatchFailed(batchID string, err error) { gb.mu.Lock() defer gb.mu.Unlock() var found *GitBatch for _, batch := range gb.pendingBatches { if batch.ID == batchID { found = batch break } } if found != nil { found.Status = "failed" found.Error = err found.ExecutedAt = time.Now() // Keep in pending (for retry logic) // Could also move to failed queue } } // QueueSize returns the current queue size func (gb *GitBatcher) QueueSize() int { gb.mu.RLock() defer gb.mu.RUnlock() return len(gb.queue) } // PendingBatchCount returns the number of pending batches func (gb *GitBatcher) PendingBatchCount() int { gb.mu.RLock() defer gb.mu.RUnlock() return len(gb.pendingBatches) } // GetStats returns batching statistics func (gb *GitBatcher) GetStats() *BatchStats { gb.mu.RLock() defer gb.mu.RUnlock() stats := *gb.stats if stats.TotalBatches > 0 { stats.AvgOpsPerBatch = float64(stats.TotalOps) / float64(stats.TotalBatches) // Estimated savings: each batch saves (ops-1) round trips stats.NetworkSavings = stats.TotalOps - stats.TotalBatches } return &stats } // GetExecutedBatches returns all executed batches func (gb *GitBatcher) GetExecutedBatches() []*GitBatch { gb.mu.RLock() defer gb.mu.RUnlock() result := make([]*GitBatch, len(gb.executedBatches)) copy(result, gb.executedBatches) return result } // GetBatchByID returns a specific batch by ID func (gb *GitBatcher) GetBatchByID(batchID string) *GitBatch { gb.mu.RLock() defer gb.mu.RUnlock() for _, batch := range gb.pendingBatches { if batch.ID == batchID { return batch } } for _, batch := range gb.executedBatches { if batch.ID == batchID { return batch } } return nil } // TimeSinceLastFlush returns time since last flush func (gb *GitBatcher) TimeSinceLastFlush() time.Duration { gb.mu.RLock() defer gb.mu.RUnlock() return time.Since(gb.lastFlushTime) } // ShouldFlush checks if batch should be flushed based on age func (gb *GitBatcher) ShouldFlush() bool { gb.mu.RLock() defer gb.mu.RUnlock() if len(gb.queue) == 0 { return false } return time.Since(gb.lastFlushTime) >= gb.maxBatchAge } // Clear clears all pending operations and batches func (gb *GitBatcher) Clear() { gb.mu.Lock() defer gb.mu.Unlock() gb.queue = make([]*GitOp, 0) gb.pendingBatches = make([]*GitBatch, 0) gb.executedBatches = make([]*GitBatch, 0) } // GetQueuedOps returns a copy of queued operations func (gb *GitBatcher) GetQueuedOps() []*GitOp { gb.mu.RLock() defer gb.mu.RUnlock() ops := make([]*GitOp, len(gb.queue)) copy(ops, gb.queue) return ops } // CalculateNetworkSavings calculates estimated network round trips saved func (gb *GitBatcher) CalculateNetworkSavings() int { gb.mu.RLock() defer gb.mu.RUnlock() totalSavings := 0 // Each batch of N operations saves N-1 round trips for _, batch := range gb.executedBatches { if len(batch.Operations) > 1 { totalSavings += len(batch.Operations) - 1 } } return totalSavings } // GetBatchInfo returns human-readable batch information func (batch *GitBatch) GetInfo() map[string]interface{} { return map[string]interface{}{ "id": batch.ID, "status": batch.Status, "op_count": len(batch.Operations), "created_at": batch.CreatedAt, "executed_at": batch.ExecutedAt, "duration": batch.ExecutedAt.Sub(batch.CreatedAt), "error": batch.Error, } }