Merge pull request #28 from rulego/dev

Dev
This commit is contained in:
Whki
2025-07-27 13:20:52 +08:00
committed by GitHub
6 changed files with 725 additions and 65 deletions
+48 -15
View File
@@ -152,24 +152,57 @@ StreamSQL 提供灵活的日志配置选项:
// 禁用日志(生产环境)
ssql := streamsql.New(streamsql.WithDiscardLog())
# 性能配置
对于生产环境,建议进行以下配置:
ssql := streamsql.New(
streamsql.WithDiscardLog(), // 禁用日志提升性能
// 其他配置选项...
)
# 与RuleGo集成
StreamSQL可以与RuleGo规则引擎无缝集成利用RuleGo丰富的组件生态
StreamSQL提供了与RuleGo规则引擎的深度集成,通过两个专用组件实现流式数据处理
// TODO: 提供RuleGo集成示例
• streamTransform (x/streamTransform) - 流转换器处理非聚合SQL查询
• streamAggregator (x/streamAggregator) - 流聚合器处理聚合SQL查询
更多详细信息和高级用法,请参阅
• 自定义函数开发指南: docs/CUSTOM_FUNCTIONS_GUIDE.md
• 快速入门指南: docs/FUNCTION_QUICK_START.md
• 完整示例: examples/
基本集成示例
package main
import (
"github.com/rulego/rulego"
"github.com/rulego/rulego/api/types"
// 注册StreamSQL组件
_ "github.com/rulego/rulego-components/external/streamsql"
)
func main() {
// 规则链配置
ruleChainJson := `{
"ruleChain": {"id": "rule01"},
"metadata": {
"nodes": [{
"id": "transform1",
"type": "x/streamTransform",
"configuration": {
"sql": "SELECT deviceId, temperature * 1.8 + 32 as temp_f FROM stream WHERE temperature > 20"
}
}, {
"id": "aggregator1",
"type": "x/streamAggregator",
"configuration": {
"sql": "SELECT deviceId, AVG(temperature) as avg_temp FROM stream GROUP BY deviceId, TumblingWindow('5s')"
}
}],
"connections": [{
"fromId": "transform1",
"toId": "aggregator1",
"type": "Success"
}]
}
}`
// 创建规则引擎
ruleEngine, _ := rulego.New("rule01", []byte(ruleChainJson))
// 发送数据
data := `{"deviceId":"sensor01","temperature":25.5}`
msg := types.NewMsg(0, "TELEMETRY", types.JSON, types.NewMetadata(), data)
ruleEngine.OnMsg(msg)
}
*/
package streamsql
+329 -49
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -38,6 +38,9 @@ type Streamsql struct {
// 性能配置模式
performanceMode string // "default", "high_performance", "low_latency", "zero_data_loss", "custom"
customConfig *types.PerformanceConfig
// 新增:同步处理模式配置
enableSyncMode bool // 是否启用同步模式(用于非聚合查询)
}
// New 创建一个新的StreamSQL实例。
@@ -190,6 +193,64 @@ func (s *Streamsql) Emit(data interface{}) {
}
}
// EmitSync 同步处理数据,立即返回处理结果。
// 仅适用于非聚合查询(如过滤、转换等),聚合查询会返回错误。
//
// 对于非聚合查询,此方法提供同步的数据处理能力,同时:
// 1. 立即返回处理结果(同步)
// 2. 触发已注册的AddSink回调异步
//
// 这确保了同步和异步模式的一致性,用户可以同时获得:
// - 立即可用的处理结果
// - 异步回调处理(用于日志、监控、持久化等)
//
// 参数:
// - data: 要处理的数据
//
// 返回值:
// - interface{}: 处理后的结果如果不匹配过滤条件返回nil
// - error: 处理错误,如果是聚合查询会返回错误
//
// 示例:
//
// // 添加日志回调
// ssql.AddSink(func(result interface{}) {
// fmt.Printf("异步日志: %v\n", result)
// })
//
// // 同步处理并立即获取结果
// result, err := ssql.EmitSync(map[string]interface{}{
// "temperature": 25.5,
// "humidity": 60.0,
// })
// if err != nil {
// // 处理错误
// } else if result != nil {
// // 立即使用处理结果
// fmt.Printf("同步结果: %v\n", result)
// // 同时异步回调也会被触发
// }
func (s *Streamsql) EmitSync(data interface{}) (interface{}, error) {
if s.stream == nil {
return nil, fmt.Errorf("stream未初始化")
}
// 检查是否为非聚合查询
if s.stream.IsAggregationQuery() {
return nil, fmt.Errorf("同步模式仅支持非聚合查询聚合查询请使用Emit()方法")
}
return s.stream.ProcessSync(data)
}
// IsAggregationQuery 检查当前查询是否为聚合查询
func (s *Streamsql) IsAggregationQuery() bool {
if s.stream == nil {
return false
}
return s.stream.IsAggregationQuery()
}
// Stream 返回底层的流处理器实例。
// 通过此方法可以访问更底层的流处理功能。
//
+258
View File
@@ -0,0 +1,258 @@
/*
* Copyright 2025 The RuleGo Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package streamsql
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestEmitSyncWithAddSink 测试EmitSync同时触发AddSink回调
func TestEmitSyncWithAddSink(t *testing.T) {
t.Run("非聚合查询同步+异步结果", func(t *testing.T) {
ssql := New()
defer ssql.Stop()
// 执行非聚合查询
sql := "SELECT temperature, humidity, temperature * 1.8 + 32 as temp_fahrenheit FROM stream WHERE temperature > 20"
err := ssql.Execute(sql)
require.NoError(t, err)
// 验证是非聚合查询
assert.False(t, ssql.IsAggregationQuery())
// 设置AddSink回调来收集异步结果
var sinkCallCount int32
var sinkResults []interface{}
var sinkResultsMux sync.Mutex // 保护sinkResults访问
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sinkCallCount, 1)
sinkResultsMux.Lock()
sinkResults = append(sinkResults, result)
sinkResultsMux.Unlock()
})
// 测试数据
testData := []map[string]interface{}{
{"temperature": 25.0, "humidity": 60.0}, // 符合条件
{"temperature": 15.0, "humidity": 70.0}, // 被过滤
{"temperature": 30.0, "humidity": 80.0}, // 符合条件
}
var syncResults []interface{}
// 处理测试数据
for _, data := range testData {
// 同步处理
result, err := ssql.EmitSync(data)
require.NoError(t, err)
if result != nil {
syncResults = append(syncResults, result)
}
}
// 等待异步回调完成
time.Sleep(100 * time.Millisecond)
// 验证同步结果
assert.Equal(t, 2, len(syncResults), "应该有2条同步结果温度>20")
// 安全读取异步回调结果
sinkResultsMux.Lock()
finalSinkResults := make([]interface{}, len(sinkResults))
copy(finalSinkResults, sinkResults)
sinkResultsMux.Unlock()
// 验证异步回调结果
finalSinkCallCount := atomic.LoadInt32(&sinkCallCount)
assert.Equal(t, int32(2), finalSinkCallCount, "AddSink应该被调用2次")
assert.Equal(t, 2, len(finalSinkResults), "应该收集到2条异步结果")
// 验证同步和异步结果的内容一致性
if len(syncResults) > 0 && len(finalSinkResults) > 0 {
// 将结果转换为可比较的格式
syncTemperatures := make([]float64, 0, len(syncResults))
syncHumidities := make([]float64, 0, len(syncResults))
asyncTemperatures := make([]float64, 0, len(finalSinkResults))
asyncHumidities := make([]float64, 0, len(finalSinkResults))
// 收集同步结果
for _, result := range syncResults {
if syncResult, ok := result.(map[string]interface{}); ok {
syncTemperatures = append(syncTemperatures, syncResult["temperature"].(float64))
syncHumidities = append(syncHumidities, syncResult["humidity"].(float64))
}
}
// 收集异步结果
for _, result := range finalSinkResults {
if sinkResultArray, ok := result.([]map[string]interface{}); ok && len(sinkResultArray) > 0 {
sinkResult := sinkResultArray[0]
asyncTemperatures = append(asyncTemperatures, sinkResult["temperature"].(float64))
asyncHumidities = append(asyncHumidities, sinkResult["humidity"].(float64))
}
}
// 验证结果集合是否一致(不考虑顺序)
assert.ElementsMatch(t, syncTemperatures, asyncTemperatures, "温度值集合应该一致")
assert.ElementsMatch(t, syncHumidities, asyncHumidities, "湿度值集合应该一致")
// 验证预期的数值是否都存在
assert.Contains(t, syncTemperatures, 25.0, "同步结果应包含25.0")
assert.Contains(t, syncTemperatures, 30.0, "同步结果应包含30.0")
assert.Contains(t, asyncTemperatures, 25.0, "异步结果应包含25.0")
assert.Contains(t, asyncTemperatures, 30.0, "异步结果应包含30.0")
}
})
t.Run("聚合查询不支持EmitSync", func(t *testing.T) {
ssql := New()
defer ssql.Stop()
// 执行聚合查询
sql := "SELECT AVG(temperature) as avg_temp FROM stream GROUP BY TumblingWindow('1s')"
err := ssql.Execute(sql)
require.NoError(t, err)
// 验证是聚合查询
assert.True(t, ssql.IsAggregationQuery())
// 尝试同步处理应该返回错误
data := map[string]interface{}{"temperature": 25.0}
result, err := ssql.EmitSync(data)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "同步模式仅支持非聚合查询")
})
t.Run("多个AddSink回调都被触发", func(t *testing.T) {
ssql := New()
defer ssql.Stop()
// 执行非聚合查询
sql := "SELECT temperature FROM stream"
err := ssql.Execute(sql)
require.NoError(t, err)
// 添加多个AddSink回调使用原子操作确保线程安全
var sink1Count, sink2Count, sink3Count int32
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sink1Count, 1)
})
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sink2Count, 1)
})
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sink3Count, 1)
})
// 处理一条数据
data := map[string]interface{}{"temperature": 25.0}
result, err := ssql.EmitSync(data)
require.NoError(t, err)
require.NotNil(t, result)
// 等待异步回调
time.Sleep(100 * time.Millisecond)
// 验证所有回调都被触发
assert.Equal(t, int32(1), atomic.LoadInt32(&sink1Count))
assert.Equal(t, int32(1), atomic.LoadInt32(&sink2Count))
assert.Equal(t, int32(1), atomic.LoadInt32(&sink3Count))
})
t.Run("过滤条件不匹配时AddSink不触发", func(t *testing.T) {
ssql := New()
defer ssql.Stop()
// 执行带过滤条件的查询
sql := "SELECT temperature FROM stream WHERE temperature > 30"
err := ssql.Execute(sql)
require.NoError(t, err)
// 添加AddSink回调
var sinkCallCount int32
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sinkCallCount, 1)
})
// 处理不符合条件的数据
data := map[string]interface{}{"temperature": 20.0} // 不符合 > 30 的条件
result, err := ssql.EmitSync(data)
require.NoError(t, err)
assert.Nil(t, result, "不符合过滤条件应该返回nil")
// 等待可能的异步回调
time.Sleep(100 * time.Millisecond)
// 验证AddSink没有被触发
assert.Equal(t, int32(0), atomic.LoadInt32(&sinkCallCount), "过滤掉的数据不应触发AddSink")
})
}
// TestEmitSyncPerformance 测试EmitSync性能包括AddSink触发
func TestEmitSyncPerformance(t *testing.T) {
ssql := New()
defer ssql.Stop()
sql := "SELECT temperature, humidity FROM stream WHERE temperature > 0"
err := ssql.Execute(sql)
require.NoError(t, err)
// 添加AddSink回调使用原子操作确保线程安全
var sinkCallCount int32
ssql.AddSink(func(result interface{}) {
atomic.AddInt32(&sinkCallCount, 1)
})
// 性能测试
testCount := 1000
start := time.Now()
for i := 0; i < testCount; i++ {
data := map[string]interface{}{
"temperature": float64(20 + i%20),
"humidity": float64(50 + i%30),
}
result, err := ssql.EmitSync(data)
require.NoError(t, err)
require.NotNil(t, result)
}
duration := time.Since(start)
// 等待所有异步回调完成
time.Sleep(200 * time.Millisecond)
t.Logf("处理 %d 条数据耗时: %v", testCount, duration)
t.Logf("平均每条数据: %v", duration/time.Duration(testCount))
t.Logf("AddSink 触发次数: %d", atomic.LoadInt32(&sinkCallCount))
// 验证性能和一致性
assert.Less(t, duration, 1*time.Second, "性能应该足够好")
assert.Equal(t, int32(testCount), atomic.LoadInt32(&sinkCallCount), "所有数据都应触发AddSink")
}
+27
View File
@@ -0,0 +1,27 @@
package reflectutil
import (
"fmt"
"reflect"
)
// SafeFieldByName 安全地获取结构体字段
func SafeFieldByName(v reflect.Value, fieldName string) (reflect.Value, error) {
// 检查Value是否有效
if !v.IsValid() {
return reflect.Value{}, fmt.Errorf("invalid value")
}
// 检查是否为结构体类型
if v.Kind() != reflect.Struct {
return reflect.Value{}, fmt.Errorf("value is not a struct, got %v", v.Kind())
}
// 安全地获取字段
field := v.FieldByName(fieldName)
if !field.IsValid() {
return reflect.Value{}, fmt.Errorf("field %s not found", fieldName)
}
return field, nil
}
+2 -1
View File
@@ -2,10 +2,11 @@ package window
import (
"fmt"
"github.com/rulego/streamsql/utils/cast"
"reflect"
"time"
"github.com/rulego/streamsql/utils/cast"
"github.com/rulego/streamsql/types"
)