Skip to content

feat: Aggregate groups#280

Merged
tbphp merged 18 commits intodevfrom
feat-aggregate-groups
Sep 30, 2025
Merged

feat: Aggregate groups#280
tbphp merged 18 commits intodevfrom
feat-aggregate-groups

Conversation

@tbphp
Copy link
Owner

@tbphp tbphp commented Sep 19, 2025

关联 Issue / Related Issue

Closes #92

变更内容 / Change Content

  • Bug 修复 / Bug fix
  • 新功能 / New feature
  • 其他改动 / Other changes

自查清单 / Checklist

  • 我已在本地测试过我的变更。 / I have tested my changes locally.
  • 我已更新了必要的文档。 / I have updated the necessary documentation.

@tbphp tbphp added this to the 1.3.0 milestone Sep 19, 2025
@tbphp tbphp self-assigned this Sep 19, 2025
@tbphp tbphp added the enhancement New feature or request label Sep 19, 2025
@tbphp tbphp changed the base branch from main to dev September 20, 2025 11:04
@tbphp tbphp linked an issue Sep 24, 2025 that may be closed by this pull request
@tbphp
Copy link
Owner Author

tbphp commented Sep 30, 2025

@coderabbitai review this PR

@coderabbitai
Copy link

coderabbitai bot commented Sep 30, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get PR details
echo "=== PR Details ==="
gh pr view 280 --json title,body,author,state,url,additions,deletions,changedFiles,labels,reviews,comments

echo -e "\n=== Files Changed ==="
gh pr diff 280 --name-only

echo -e "\n=== PR Diff ==="
gh pr diff 280

Length of output: 223800


@tbphp Thank you for this comprehensive PR! This is a substantial feature addition that introduces aggregate groups with weighted load balancing. Here's my detailed review:

🎯 Overall Architecture - Excellent!

The service layer separation is well-designed:

  • GroupService - handles CRUD operations
  • AggregateGroupService - manages sub-group relationships
  • SubGroupManager - implements weighted round-robin selection

🔍 Key Observations

Strengths

  1. Proper validation hierarchy - Prevents aggregate groups from containing other aggregates, avoiding circular dependencies
  2. Weighted round-robin algorithm - Smooth distribution implementation in selector.selectByWeight()
  3. Statistics aggregation - Parent group tracking in request logs enables proper metric rollup
  4. I18n coverage - Complete translations for all three supported languages
  5. Frontend UX - Visual distinction between standard and aggregate groups with dashed borders

⚠️ Potential Issues

1. Database Migration Risk (High Priority)

The PR adds GroupSubGroup table but I don't see explicit migration handling for the new group_type and parent_group_* fields:

// internal/models/types.go
GroupType string `gorm:"type:varchar(50);default:'standard'"` // ✅ Has default
ParentGroupID uint `gorm:"index"` // ⚠️ No NOT NULL, good
ParentGroupName string `gorm:"type:varchar(255);index"` // ⚠️ Could be empty string

Recommendation: Ensure existing groups get group_type='standard' via database migration or application startup logic.

2. Selector Cache Invalidation Timing

In SubGroupManager.RebuildSelectors(), the rebuild happens in GroupManager.Initialize() via afterReload:

// internal/services/group_manager.go:109
afterReload := func(newCache map[string]*models.Group) {
    gm.subGroupManager.RebuildSelectors(newCache)
}

Concern: If a sub-group is deleted mid-request, the selector might reference a non-existent group temporarily.

Suggestion: Add defensive checks in selector.selectNext() to handle missing groups gracefully.

3. Weight Validation Inconsistency

Frontend allows weight 0-1000, backend validates the same:

// internal/services/aggregate_group_service.go:51
if input.Weight > 1000 {
    return nil, NewI18nError(app_errors.ErrValidation, "validation.sub_group_weight_max_exceeded", nil)
}

But in SubGroupManager, weight 0 is filtered:

// internal/services/group_manager.go:51
if err := gm.db.Where("weight > 0").Find(&allSubGroups).Error; err != nil {

Question: Should weight=0 be treated as "disabled" or "invalid"? The UI shows it as disabled, but the database constraint isn't explicit.

Recommendation: Document this behavior in code comments.

4. Validation Endpoint Consistency Check

The code enforces that all sub-groups must have the same validation endpoint:

// internal/services/aggregate_group_service.go:87
if validationEndpoint != utils.GetValidationEndpoint(&sg) {
    return nil, NewI18nError(app_errors.ErrValidation, "validation.sub_group_validation_endpoint_mismatch", nil)
}

Edge Case: If a sub-group's validation_endpoint is updated after being added to an aggregate, this could break the aggregate group's validation.

Suggestion: Add a pre-update hook or validation to check if a group is referenced by aggregates before allowing channel_type or validation_endpoint changes. (Note: I see this is partially handled in GroupService.UpdateGroup() lines 285-304, good!)

5. Error Handling in Proxy Selection

// internal/proxy/server.go:67
subGroupName, err := ps.subGroupManager.SelectSubGroup(originalGroup)
if err != nil {
    logrus.WithFields(...).Error("Failed to select sub-group from aggregate")
    response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, "No available sub-groups"))
    return
}

Concern: The error message "No available sub-groups" might be misleading if the error is actually a selector configuration issue rather than lack of keys.

Suggestion: Differentiate between "no sub-groups configured" vs "all sub-groups have no keys".

6. Statistics Query Performance

The new getAggregateGroupStats() queries group_hourly_stats by group_id which now includes aggregate group IDs:

// internal/services/request_log_service.go:258
if log.ParentGroupID > 0 {
    parentKey := struct { Time time.Time; GroupID uint }{Time: hourlyTime, GroupID: log.ParentGroupID}
    // ...
}

Observation: This doubles the writes to group_hourly_stats (once for sub-group, once for parent). With high traffic, this could become a bottleneck.

Suggestion: Consider batch updates or monitoring write performance.

🐛 Minor Issues

  1. package-lock.json deleted - Line 1 shows package-lock.json was completely removed. This might cause dependency version drift for web developers.

  2. Unused imports - Some files might have unused imports after refactoring (e.g., fmt in group_handler.go line 14).

  3. Magic number - The weight max value 1000 appears in multiple places without a constant definition.

    Suggestion:

    const MaxSubGroupWeight = 1000

💡 Enhancement Suggestions

  1. Add health check for aggregate groups - A periodic job to verify all sub-groups still exist and have valid configurations.

  2. Metrics/observability - Add metrics for:

    • Sub-group selection attempts per aggregate
    • Weight distribution effectiveness
    • Cache hit rate for selectors
  3. UI preview - The weight preview percentage is great! Consider adding a visual bar chart showing the distribution across all sub-groups.

  4. Documentation - Consider adding:

    • README section explaining aggregate groups concept
    • Architecture diagram showing the selection flow
    • Best practices for weight configuration

🎉 Excellent Work!

Despite the areas for improvement noted above, this is a well-architected feature:

  • Clean separation of concerns
  • Comprehensive validation
  • Good error handling foundation
  • Complete i18n support
  • Thoughtful UX considerations

The weighted round-robin implementation is solid, and the validation endpoint consistency check shows attention to edge cases.

Recommendation: Address the high-priority items (database migration verification, error message clarity) before merging, and consider the enhancements for future iterations.

Great job on this feature! 🚀

@coderabbitai
Copy link

coderabbitai bot commented Sep 30, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get PR details including title, description, status
gh pr view 280 --json title,body,state,headRefName,baseRefName,author,commits,files,additions,deletions

# Get the PR diff to see what changed
gh pr diff 280

# Get list of files changed
gh pr view 280 --json files --jq '.files[] | "\(.path) (+\(.additions)/-\(.deletions))"'

Length of output: 232069


PR Review: Aggregate Groups Feature

@tbphp Thank you for this comprehensive implementation of the aggregate groups feature! This is a substantial addition that introduces group pooling with weighted round-robin selection. Here's my detailed review:

🎯 Overview

This PR successfully implements aggregate groups functionality, allowing multiple standard groups to be combined into a single aggregate group with configurable weights for traffic distribution.

✅ Strengths

1. Clean Architecture

  • Excellent separation of concerns with dedicated services (AggregateGroupService, GroupService, SubGroupManager)
  • Well-structured dependency injection through the container
  • Clear distinction between aggregate and standard group logic

2. Database Design

  • GroupSubGroup junction table properly models the many-to-many relationship
  • Weight field enables flexible traffic distribution
  • Proper indexing with uniqueIndex:idx_group_sub

3. Frontend Implementation

  • Comprehensive Vue components for managing aggregate groups
  • Visual weight distribution with progress bars
  • Good UX with real-time validation and preview functionality

4. Internationalization

  • Complete i18n coverage for all three languages (en-US, ja-JP, zh-CN)
  • Consistent messaging across the application

🔍 Key Observations & Suggestions

1. Database Migration Missing

The PR adds a new GroupSubGroup model but I don't see a migration file. While AutoMigrate will handle this, consider:

// Add explicit migration for better version control
if err := a.db.AutoMigrate(&models.GroupSubGroup{}); err != nil {
    return fmt.Errorf("failed to migrate GroupSubGroup: %w", err)
}

2. Weighted Round-Robin Implementation

The selector.selectByWeight() uses smooth weighted round-robin, which is excellent. However, consider adding comments explaining the algorithm:

// selectByWeight implements smooth weighted round-robin algorithm (nginx style)
// This ensures better distribution than simple weighted random selection
func (s *selector) selectByWeight() *subGroupItem {
    // ... implementation
}

3. Validation Endpoint Consistency

In aggregate_group_service.go, the validation ensures all sub-groups have consistent validation endpoints:

if validationEndpoint != utils.GetValidationEndpoint(&sg) {
    return nil, NewI18nError(app_errors.ErrValidation, 
        "validation.sub_group_validation_endpoint_mismatch", nil)
}

This is good! But consider documenting why this is required in a comment.

4. Circular Dependency Prevention

The validation prevents aggregates from containing other aggregates:

if sg.GroupType == "aggregate" {
    return nil, NewI18nError(app_errors.ErrValidation, 
        "validation.sub_group_cannot_be_aggregate", nil)
}

However, there's no check to prevent creating cycles (e.g., Group A → Group B → Group A through multiple aggregate layers). Consider adding:

// TODO: Add cycle detection to prevent complex circular references
// when multiple aggregate groups reference each other's sub-groups

5. Statistics Aggregation

In group_service.go, aggregate groups now track statistics:

if originalGroup.GroupType == "aggregate" && originalGroup.ID != group.ID {
    logEntry.ParentGroupID = originalGroup.ID
    logEntry.ParentGroupName = originalGroup.Name
}

Great! This allows tracking both aggregate and sub-group metrics. Consider adding a clarifying comment about the hierarchical logging strategy.

6. Error Handling Pattern

The new I18nError type is a good pattern:

type I18nError struct {
    APIError  *app_errors.APIError
    MessageID string
    Template  map[string]any
}

This is well-designed for multilingual error messages. Consider documenting when to use this vs. plain APIError.

7. Frontend State Management

In Keys.vue, the refreshGroupsAndSelect function handles multiple scenarios well:

async function refreshGroupsAndSelect(targetGroupId?: number, selectFirst = true) {
    await loadGroups();
    // ... complex selection logic
}

Consider simplifying this with early returns to reduce nesting.

8. CSS Styling

The aggregate group styling uses dashed borders to differentiate from standard groups:

.group-item.aggregate {
  border-style: dashed;
  background: linear-gradient(135deg, rgba(102, 126, 234, 0.02) 0%, rgba(102, 126, 234, 0.05) 100%);
}

Nice visual distinction! The gradients look good in both light and dark modes.

9. Performance Consideration

In SubGroupManager.RebuildSelectors(), all selectors are rebuilt on every group cache update:

func (m *SubGroupManager) RebuildSelectors(groups map[string]*models.Group) {
    m.mu.Lock()
    m.selectors = make(map[uint]*selector)
    m.mu.Unlock()
    // ...
}

For systems with many aggregate groups, consider incremental updates instead of full rebuilds.

10. Missing Tests

I don't see unit tests for the new services. Consider adding tests for:

  • Weighted round-robin selection algorithm
  • Sub-group validation logic
  • Cycle detection (if added)
  • Edge cases (empty weights, single sub-group, etc.)

🐛 Potential Issues

1. Race Condition in Selector

In subgroup_manager.go:

func (m *SubGroupManager) getSelector(group *models.Group) *selector {
    m.mu.RLock()
    if sel, exists := m.selectors[group.ID]; exists {
        m.mu.RUnlock()
        return sel
    }
    m.mu.RUnlock()
    
    m.mu.Lock()
    defer m.mu.Unlock()
    
    // Check again after acquiring write lock
    if sel, exists := m.selectors[group.ID]; exists {
        return sel
    }
    // ...
}

Good use of double-checked locking! However, there's still a potential race in selector.selectNext() which uses s.mu separately. The two locks don't coordinate, which is fine but worth documenting.

2. Deleted package-lock.json

The PR removes package-lock.json:

-{
-  "name": "gpt-load",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {}
-}

This file should typically be kept in version control. Was this intentional?

📝 Minor Suggestions

  1. Add JSDoc comments to TypeScript interfaces in models.ts
  2. Extract magic numbers like 1000 (max weight) to constants
  3. Consider adding a migration script example in documentation
  4. Add loading states for sub-group operations in the UI
  5. Document the weighted round-robin algorithm in README or docs

🎨 Code Quality

  • Clean, readable code with good naming conventions
  • Consistent error handling patterns
  • Good use of TypeScript for type safety
  • Proper use of Vue 3 Composition API

🚀 Recommendations

  1. Add unit tests for critical paths
  2. Consider adding integration tests for aggregate group selection
  3. Document the aggregate groups feature in user-facing documentation
  4. Add metrics/monitoring for aggregate group performance
  5. Consider adding health checks for sub-group availability

📊 Statistics Impact

The addition of ParentGroupID and ParentGroupName to logs is excellent for tracking:

  • Which aggregate group handled the request
  • Which sub-group actually processed it
  • Performance metrics at both levels

🎉 Summary

This is a well-architected feature with clean separation of concerns and comprehensive frontend support. The weighted round-robin implementation is solid, and the UI provides good visibility into configuration and status. With some minor improvements (tests, documentation, potential race condition handling), this will be production-ready.

Rating: 8.5/10 - Excellent implementation with room for testing and documentation improvements.

Great work on this complex feature! 🚀

Copy link
Owner Author

@tbphp tbphp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reviewed.

@tbphp tbphp merged commit 7c30bbf into dev Sep 30, 2025
1 check passed
@tbphp tbphp deleted the feat-aggregate-groups branch September 30, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

1 participant