Compare commits
No commits in common. "develop" and "feature-jointDebuggingDemo" have entirely different histories.
develop
...
feature-jo
|
|
@ -27,17 +27,3 @@ go.work
|
|||
/log/
|
||||
# Shield config files in the configs folder
|
||||
/configs/**/*.yaml
|
||||
/configs/**/*.pem
|
||||
|
||||
# ai config
|
||||
.cursor/
|
||||
.claude/
|
||||
.codewhale/
|
||||
.cursorrules
|
||||
.copilot/
|
||||
.chatgpt/
|
||||
.ai_history/
|
||||
.vector_cache/
|
||||
ai-debug.log
|
||||
*.patch
|
||||
*.diff
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
// Package common define common error variables
|
||||
package common
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrUnsupportedParameterField indicates that a requested parameter field is not supported.
|
||||
ErrUnsupportedParameterField = errors.New("unsupported parameter field")
|
||||
// ErrInvalidParameterToken indicates that a token cannot represent a parameter.
|
||||
ErrInvalidParameterToken = errors.New("invalid parameter token")
|
||||
// ErrParameterTokenNotFound indicates that no parameter matches the token hierarchy.
|
||||
ErrParameterTokenNotFound = errors.New("parameter token not found")
|
||||
// ErrAmbiguousParameterToken indicates that a token matches more than one parameter.
|
||||
ErrAmbiguousParameterToken = errors.New("ambiguous parameter token")
|
||||
// ErrUnsupportedMeasurementField define error of unsupport measurement field
|
||||
ErrUnsupportedMeasurementField = errors.New("unsupported measurement field")
|
||||
// ErrInvalidMeasurementToken indicates that a token cannot represent a measurement.
|
||||
ErrInvalidMeasurementToken = errors.New("invalid measurement token")
|
||||
// ErrMeasurementTokenNotFound indicates that no measurement matches the token hierarchy.
|
||||
ErrMeasurementTokenNotFound = errors.New("measurement token not found")
|
||||
// ErrAmbiguousMeasurementToken indicates that a token matches more than one measurement.
|
||||
ErrAmbiguousMeasurementToken = errors.New("ambiguous measurement token")
|
||||
)
|
||||
|
|
@ -16,12 +16,6 @@ var (
|
|||
|
||||
// ErrFoundTargetFailed define variable to returned when the specific database table cannot be identified using the provided token info.
|
||||
ErrFoundTargetFailed = newError(40004, "found target table by token failed")
|
||||
// ErrSubTargetRepeat define variable to indicates subscription target already exist in list
|
||||
ErrSubTargetRepeat = newError(40005, "subscription target already exist in list")
|
||||
// ErrSubTargetNotFound define variable to indicates can not find measurement by subscription target
|
||||
ErrSubTargetNotFound = newError(40006, "found measuremnet by subscription target failed")
|
||||
// ErrCancelSubTargetMissing define variable to indicates cancel a not exist subscription target
|
||||
ErrCancelSubTargetMissing = newError(40007, "cancel a not exist subscription target")
|
||||
|
||||
// ErrDBQueryFailed define variable to represents a generic failure during a PostgreSQL SELECT or SCAN operation.
|
||||
ErrDBQueryFailed = newError(50001, "query postgres database data failed")
|
||||
|
|
@ -38,9 +32,6 @@ var (
|
|||
// ErrCommitTxFailed indicates that the PostgreSQL transaction could not be committed successfully.
|
||||
ErrCommitTxFailed = newError(50005, "postgres database transaction commit failed")
|
||||
|
||||
// ErrMeasurementValueUpdateFailed indicates that a manual measurement value transaction failed.
|
||||
ErrMeasurementValueUpdateFailed = newError(50006, "measurement manual value update failed")
|
||||
|
||||
// ErrCachedQueryFailed define variable to indicates an error occurred while attempting to fetch data from the Redis cache.
|
||||
ErrCachedQueryFailed = newError(60001, "query redis cached data failed")
|
||||
|
||||
|
|
@ -49,10 +40,4 @@ var (
|
|||
|
||||
// ErrCacheQueryFailed define variable to indicates query cached data by token failed.
|
||||
ErrCacheQueryFailed = newError(60003, "query cached data by token failed")
|
||||
|
||||
// ErrTaskNotFound indicates the async task with the given ID does not exist.
|
||||
ErrTaskNotFound = newError(40008, "async task not found")
|
||||
|
||||
// ErrTaskCannotCancel indicates the task is already running or completed and cannot be cancelled.
|
||||
ErrTaskCannotCancel = newError(40009, "task cannot be cancelled, already running or completed")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ func Wrap(msg string, err error) *AppError {
|
|||
return appErr
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause for errors.Is and errors.As traversal.
|
||||
func (e *AppError) Unwrap() error {
|
||||
// UnWrap define func return the error wrapped in structure
|
||||
func (e *AppError) UnWrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +139,10 @@ func (e *AppError) SetMsg(msg string) *AppError {
|
|||
}
|
||||
|
||||
type formattedErr struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Cause any `json:"cause"`
|
||||
Occurred string `json:"occurred"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Cause interface{} `json:"cause"`
|
||||
Occurred string `json:"occurred"`
|
||||
}
|
||||
|
||||
// toStructuredError define func convert AppError to structured error for better readability
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// Package common define common error variables
|
||||
package common
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrUnknowEventActionCommand define error of unknown event action command
|
||||
var ErrUnknowEventActionCommand = errors.New("unknown action command")
|
||||
|
||||
// ErrExecEventActionFailed define error of execute event action failed
|
||||
var ErrExecEventActionFailed = errors.New("exec event action func failed")
|
||||
|
|
@ -42,13 +42,12 @@ var baseCurrentFunc = func(archorValue float64, args ...float64) float64 {
|
|||
}
|
||||
|
||||
// SelectAnchorCalculateFuncAndParams define select anchor func and anchor calculate value by component type 、 anchor name and component data
|
||||
func SelectAnchorCalculateFuncAndParams(componentType int, anchorName string, componentData map[string]any) (func(archorValue float64, args ...float64) float64, []float64) {
|
||||
func SelectAnchorCalculateFuncAndParams(componentType int, anchorName string, componentData map[string]interface{}) (func(archorValue float64, args ...float64) float64, []float64) {
|
||||
if componentType == constants.DemoType {
|
||||
switch anchorName {
|
||||
case "voltage":
|
||||
if anchorName == "voltage" {
|
||||
resistance := componentData["resistance"].(float64)
|
||||
return baseVoltageFunc, []float64{resistance}
|
||||
case "current":
|
||||
} else if anchorName == "current" {
|
||||
resistance := componentData["resistance"].(float64)
|
||||
return baseCurrentFunc, []float64{resistance}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package config
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
|
@ -20,21 +19,6 @@ type ServiceConfig struct {
|
|||
ServiceAddr string `mapstructure:"service_addr"`
|
||||
ServiceName string `mapstructure:"service_name"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
DeployEnv string `mapstructure:"deploy_env"`
|
||||
}
|
||||
|
||||
// RabbitMQConfig define config struct of RabbitMQ config
|
||||
type RabbitMQConfig struct {
|
||||
CACertPath string `mapstructure:"ca_cert_path"`
|
||||
ClientKeyPath string `mapstructure:"client_key_path"`
|
||||
ClientKeyPassword string `mapstructure:"client_key_password"`
|
||||
ClientCertPath string `mapstructure:"client_cert_path"`
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||
ServerName string `mapstructure:"server_name"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
|
||||
// KafkaConfig define config struct of kafka config
|
||||
|
|
@ -56,33 +40,24 @@ type PostgresConfig struct {
|
|||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
// LokiConfig define config struct of loki direct-push (used in development mode)
|
||||
type LokiConfig struct {
|
||||
Endpoint string `mapstructure:"endpoint"` // empty disables direct push
|
||||
Labels map[string]string `mapstructure:"labels"`
|
||||
}
|
||||
|
||||
// LoggerConfig define config struct of zap logger config
|
||||
type LoggerConfig struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
Level string `mapstructure:"level"`
|
||||
FilePath string `mapstructure:"filepath"` // empty disables file rotation in container modes
|
||||
MaxSize int `mapstructure:"maxsize"`
|
||||
MaxBackups int `mapstructure:"maxbackups"`
|
||||
MaxAge int `mapstructure:"maxage"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
Loki LokiConfig `mapstructure:"loki"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
Level string `mapstructure:"level"`
|
||||
FilePath string `mapstructure:"filepath"`
|
||||
MaxSize int `mapstructure:"maxsize"`
|
||||
MaxBackups int `mapstructure:"maxbackups"`
|
||||
MaxAge int `mapstructure:"maxage"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// RedisConfig define config struct of redis config
|
||||
type RedisConfig struct {
|
||||
Addr string `mapstructure:"addr"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"poolsize"`
|
||||
DialTimeout int `mapstructure:"dial_timeout"`
|
||||
ReadTimeout int `mapstructure:"read_timeout"`
|
||||
WriteTimeout int `mapstructure:"write_timeout"`
|
||||
Addr string `mapstructure:"addr"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"poolsize"`
|
||||
Timeout int `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
// AntsConfig define config struct of ants pool config
|
||||
|
|
@ -99,37 +74,18 @@ type DataRTConfig struct {
|
|||
Method string `mapstructure:"polling_api_method"`
|
||||
}
|
||||
|
||||
// OtelConfig define config struct of OpenTelemetry tracing
|
||||
type OtelConfig struct {
|
||||
Endpoint string `mapstructure:"endpoint"` // e.g. "localhost:4318"
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
}
|
||||
|
||||
// AsyncTaskConfig define config struct of asynchronous task system
|
||||
type AsyncTaskConfig struct {
|
||||
WorkerPoolSize int `mapstructure:"worker_pool_size"`
|
||||
QueueConsumerCount int `mapstructure:"queue_consumer_count"`
|
||||
MaxRetryCount int `mapstructure:"max_retry_count"`
|
||||
RetryInitialDelay time.Duration `mapstructure:"retry_initial_delay"`
|
||||
RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`
|
||||
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
|
||||
}
|
||||
|
||||
// ModelRTConfig define config struct of model runtime server
|
||||
type ModelRTConfig struct {
|
||||
BaseConfig `mapstructure:"base"`
|
||||
ServiceConfig `mapstructure:"service"`
|
||||
PostgresConfig `mapstructure:"postgres"`
|
||||
RabbitMQConfig `mapstructure:"rabbitmq"`
|
||||
KafkaConfig `mapstructure:"kafka"`
|
||||
LoggerConfig `mapstructure:"logger"`
|
||||
AntsConfig `mapstructure:"ants"`
|
||||
DataRTConfig `mapstructure:"dataRT"`
|
||||
LockerRedisConfig RedisConfig `mapstructure:"locker_redis"`
|
||||
StorageRedisConfig RedisConfig `mapstructure:"storage_redis"`
|
||||
AsyncTaskConfig AsyncTaskConfig `mapstructure:"async_task"`
|
||||
OtelConfig OtelConfig `mapstructure:"otel"`
|
||||
PostgresDBURI string `mapstructure:"-"`
|
||||
PostgresDBURI string `mapstructure:"-"`
|
||||
}
|
||||
|
||||
// ReadAndInitConfig return modelRT project config struct
|
||||
|
|
@ -145,9 +101,6 @@ func ReadAndInitConfig(configDir, configName, configType string) (modelRTConfig
|
|||
panic(err)
|
||||
}
|
||||
|
||||
config.BindEnv("postgres.password", "POSTGRES_PASSWORD")
|
||||
config.BindEnv("service.secret_key", "SERVICE_SECRET_KEY")
|
||||
|
||||
if err := config.Unmarshal(&modelRTConfig); err != nil {
|
||||
panic(fmt.Sprintf("unmarshal modelRT config failed:%s\n", err.Error()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
const (
|
||||
// CodeSuccess define constant to indicates that the API was successfully processed
|
||||
CodeSuccess = 20000
|
||||
// CodeInvalidParamFailed define constant to indicates request parameter parsing failed
|
||||
CodeInvalidParamFailed = 40001
|
||||
// CodeDBQueryFailed define constant to indicates database query operation failed
|
||||
CodeDBQueryFailed = 50001
|
||||
// CodeDBUpdateailed define constant to indicates database update operation failed
|
||||
CodeDBUpdateailed = 50002
|
||||
// CodeRedisQueryFailed define constant to indicates redis query operation failed
|
||||
CodeRedisQueryFailed = 60001
|
||||
// CodeRedisUpdateFailed define constant to indicates redis update operation failed
|
||||
CodeRedisUpdateFailed = 60002
|
||||
)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
const (
|
||||
// CodeSuccess define constant to indicates that the API was successfully processed
|
||||
CodeSuccess = 20000
|
||||
// CodeInvalidParamFailed define constant to indicates request parameter parsing failed
|
||||
CodeInvalidParamFailed = 40001
|
||||
// CodeFoundTargetFailed define variable to returned when the specific database table cannot be identified using the provided token info.
|
||||
CodeFoundTargetFailed = 40004
|
||||
// CodeSubTargetRepeat define variable to indicates subscription target already exist in list
|
||||
CodeSubTargetRepeat = 40005
|
||||
// CodeSubTargetNotFound define variable to indicates can not find measurement by subscription target
|
||||
CodeSubTargetNotFound = 40006
|
||||
// CodeCancelSubTargetMissing define variable to indicates cancel a not exist subscription target
|
||||
CodeCancelSubTargetMissing = 40007
|
||||
// CodeUpdateSubTargetMissing define variable to indicates update a not exist subscription target
|
||||
CodeUpdateSubTargetMissing = 40008
|
||||
// CodeAppendSubTargetMissing define variable to indicates append a not exist subscription target
|
||||
CodeAppendSubTargetMissing = 40009
|
||||
// CodeUnsupportSubOperation define variable to indicates append a not exist subscription target
|
||||
CodeUnsupportSubOperation = 40010
|
||||
// CodeDBQueryFailed define constant to indicates database query operation failed
|
||||
CodeDBQueryFailed = 50001
|
||||
// CodeDBUpdateailed define constant to indicates database update operation failed
|
||||
CodeDBUpdateailed = 50002
|
||||
// CodeRedisQueryFailed define constant to indicates redis query operation failed
|
||||
CodeRedisQueryFailed = 60001
|
||||
// CodeRedisUpdateFailed define constant to indicates redis update operation failed
|
||||
CodeRedisUpdateFailed = 60002
|
||||
)
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
// ClientTokenContextName is the Gin key used for the configured client token.
|
||||
const ClientTokenContextName = "client_token"
|
||||
|
||||
type contextKey string
|
||||
|
||||
// MeasurementUUIDKey define measurement uuid key into context
|
||||
const MeasurementUUIDKey contextKey = "measurement_uuid"
|
||||
|
||||
// CtxKeyClientToken is the typed standard-library context key for client token propagation.
|
||||
const CtxKeyClientToken contextKey = ClientTokenContextName
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
// DataObjectType identifies the kind of object represented by a data object token.
|
||||
type DataObjectType string
|
||||
|
||||
const (
|
||||
// DataObjectTypeParameter represents a component parameter.
|
||||
DataObjectTypeParameter DataObjectType = "parameter"
|
||||
// DataObjectTypeMeasurement represents a component measurement.
|
||||
DataObjectTypeMeasurement DataObjectType = "measurement"
|
||||
)
|
||||
|
||||
const (
|
||||
// MeasurementModeManual indicates that manual value entry is enabled.
|
||||
MeasurementModeManual int16 = 0
|
||||
// MeasurementModeAutomatic indicates that the measurement runs automatically.
|
||||
MeasurementModeAutomatic int16 = 1
|
||||
)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
const (
|
||||
// DevelopmentDeployMode define development operator environment for modelRT project
|
||||
DevelopmentDeployMode = "development"
|
||||
// DebugDeployMode define debug operator environment for modelRT project
|
||||
DebugDeployMode = "debug"
|
||||
// ProductionDeployMode define production operator environment for modelRT project
|
||||
ProductionDeployMode = "production"
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Package common define common error variables
|
||||
package common
|
||||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
import "errors"
|
||||
|
||||
|
|
@ -1,97 +1,31 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
// EvenvtType define event type
|
||||
type EvenvtType int
|
||||
|
||||
const (
|
||||
// EventGeneralHard define gereral hard event type
|
||||
EventGeneralHard EvenvtType = iota
|
||||
// EventGeneralPlatformSoft define gereral platform soft event type
|
||||
EventGeneralPlatformSoft
|
||||
// EventGeneralApplicationSoft define gereral application soft event type
|
||||
EventGeneralApplicationSoft
|
||||
// EventWarnHard define warn hard event type
|
||||
EventWarnHard
|
||||
// EventWarnPlatformSoft define warn platform soft event type
|
||||
EventWarnPlatformSoft
|
||||
// EventWarnApplicationSoft define warn application soft event type
|
||||
EventWarnApplicationSoft
|
||||
// EventCriticalHard define critical hard event type
|
||||
EventCriticalHard
|
||||
// EventCriticalPlatformSoft define critical platform soft event type
|
||||
EventCriticalPlatformSoft
|
||||
// EventCriticalApplicationSoft define critical application soft event type
|
||||
EventCriticalApplicationSoft
|
||||
)
|
||||
|
||||
// IsGeneral define fucn to check event type is general
|
||||
func IsGeneral(eventType EvenvtType) bool {
|
||||
return eventType < 3
|
||||
}
|
||||
|
||||
// IsWarning define fucn to check event type is warn
|
||||
func IsWarning(eventType EvenvtType) bool {
|
||||
return eventType >= 3 && eventType <= 5
|
||||
}
|
||||
|
||||
// IsCritical define fucn to check event type is critical
|
||||
func IsCritical(eventType EvenvtType) bool {
|
||||
return eventType >= 6
|
||||
}
|
||||
|
||||
const (
|
||||
// EventFromStation define event from station type
|
||||
EventFromStation = "station"
|
||||
// EventFromPlatform define event from platform type
|
||||
EventFromPlatform = "platform"
|
||||
// EventFromOthers define event from others type
|
||||
EventFromOthers = "others"
|
||||
// TIBreachTriggerType define out of bounds type constant
|
||||
TIBreachTriggerType = "trigger"
|
||||
)
|
||||
|
||||
const (
|
||||
// EventStatusHappended define status for event record when event just happened, no data attached yet
|
||||
EventStatusHappended = iota
|
||||
// EventStatusDataAttached define status for event record when event data attached, ready to be sent
|
||||
EventStatusDataAttached
|
||||
// EventStatusReported define status for event record when event reported to downstream, no matter it's successful or failed
|
||||
EventStatusReported
|
||||
// EventStatusConfirmed define status for event record when event confirmed by operator or CIM
|
||||
EventStatusConfirmed
|
||||
// EventStatusClosed define status for event record when event closed due to condition recovery or manual close
|
||||
EventStatusClosed
|
||||
// TelemetryUpLimit define telemetry upper limit
|
||||
TelemetryUpLimit = "up"
|
||||
// TelemetryUpUpLimit define telemetry upper upper limit
|
||||
TelemetryUpUpLimit = "upup"
|
||||
|
||||
// TelemetryDownLimit define telemetry limit
|
||||
TelemetryDownLimit = "down"
|
||||
// TelemetryDownDownLimit define telemetry lower lower limit
|
||||
TelemetryDownDownLimit = "downdown"
|
||||
)
|
||||
|
||||
const (
|
||||
// EventExchangeName define exchange name for event alarm message
|
||||
EventExchangeName = "event-exchange"
|
||||
// EventDeadExchangeName define dead letter exchange name for event alarm message
|
||||
EventDeadExchangeName = "event-dead-letter-exchange"
|
||||
// TelesignalRaising define telesignal raising edge
|
||||
TelesignalRaising = "raising"
|
||||
// TelesignalFalling define telesignal falling edge
|
||||
TelesignalFalling = "falling"
|
||||
)
|
||||
|
||||
const (
|
||||
// EventUpDownRoutingKey define routing key for up or down limit event alarm message
|
||||
EventUpDownRoutingKey = "event.#"
|
||||
// EventUpDownDeadRoutingKey define dead letter routing key for up or down limit event alarm message
|
||||
EventUpDownDeadRoutingKey = "event.#"
|
||||
// EventUpDownQueueName define queue name for up or down limit event alarm message
|
||||
EventUpDownQueueName = "event-up-down-queue"
|
||||
// EventUpDownDeadQueueName define dead letter queue name for event alarm message
|
||||
EventUpDownDeadQueueName = "event-dead-letter-queue"
|
||||
)
|
||||
|
||||
const (
|
||||
// EventGeneralUpDownLimitCategroy define category for general up and down limit event
|
||||
EventGeneralUpDownLimitCategroy = "event.general.updown.limit"
|
||||
// EventWarnUpDownLimitCategroy define category for warn up and down limit event
|
||||
EventWarnUpDownLimitCategroy = "event.warn.updown.limit"
|
||||
// EventCriticalUpDownLimitCategroy define category for critical up and down limit event
|
||||
EventCriticalUpDownLimitCategroy = "event.critical.updown.limit"
|
||||
)
|
||||
|
||||
const (
|
||||
// EventTaskGeneralTestCategory define category for test task event
|
||||
EventTaskGeneralTestCategory = "event.general.task.test"
|
||||
// EventTaskGeneralTopologyAnalyzeCategory define category for topology analyze task event
|
||||
EventTaskGeneralTopologyAnalyzeCategory = "event.general.task.topology_analyze"
|
||||
// MinBreachCount define min breach count of real time data
|
||||
MinBreachCount = 10
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ const (
|
|||
|
||||
// channel name suffix
|
||||
const (
|
||||
ChannelSuffixP = "p"
|
||||
ChannelSuffixQ = "q"
|
||||
ChannelSuffixS = "s"
|
||||
ChannelSuffixPF = "pf"
|
||||
ChannelSuffixF = "f"
|
||||
ChannelSuffixDeltaF = "df"
|
||||
ChannelSuffixUAB = "uab"
|
||||
ChannelSuffixUBC = "ubc"
|
||||
ChannelSuffixUCA = "uca"
|
||||
ChannelSuffixP = "P"
|
||||
ChannelSuffixQ = "Q"
|
||||
ChannelSuffixS = "S"
|
||||
ChannelSuffixPS = "PS"
|
||||
ChannelSuffixF = "F"
|
||||
ChannelSuffixDeltaF = "deltaF"
|
||||
ChannelSuffixUAB = "UAB"
|
||||
ChannelSuffixUBC = "UBC"
|
||||
ChannelSuffixUCA = "UCA"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
const (
|
||||
// MessageExchangeName define exchange name for message
|
||||
MessageExchangeName = "message-exchange"
|
||||
// MessageDeadExchangeName define dead letter exchange name for message
|
||||
MessageDeadExchangeName = "message-dead-letter-exchange"
|
||||
)
|
||||
|
||||
const (
|
||||
// MessageRoutingKey define binding routing key pattern for the message queue (matches all message.* categories)
|
||||
MessageRoutingKey = "message.#"
|
||||
// MessageDeadRoutingKey define binding routing key for the message dead letter queue
|
||||
MessageDeadRoutingKey = "#"
|
||||
// MessageQueueName define queue name for message
|
||||
MessageQueueName = "message-queue"
|
||||
// MessageDeadQueueName define dead letter queue name for message
|
||||
MessageDeadQueueName = "message-dead-letter-queue"
|
||||
)
|
||||
|
||||
const (
|
||||
// MessageTaskSubmittedCategory define category for task submitted message
|
||||
MessageTaskSubmittedCategory = "message.task.submitted"
|
||||
// MessageTaskRunningCategory define category for task running message
|
||||
MessageTaskRunningCategory = "message.task.running"
|
||||
// MessageTaskCompletedCategory define category for task completed message
|
||||
MessageTaskCompletedCategory = "message.task.completed"
|
||||
// MessageTaskFailedCategory define category for task failed message
|
||||
MessageTaskFailedCategory = "message.task.failed"
|
||||
// MessageTaskCancelledCategory define category for task cancelled message
|
||||
MessageTaskCancelledCategory = "message.task.cancelled"
|
||||
)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
import "strings"
|
||||
|
||||
var supportedParameterTableSuffixes = [...]string{
|
||||
"base_extend",
|
||||
"rated",
|
||||
"setup",
|
||||
"model",
|
||||
"stable",
|
||||
"craft",
|
||||
"integrity",
|
||||
"behavior",
|
||||
}
|
||||
|
||||
// IsSupportedParameterTableName reports whether a dynamic parameter table has
|
||||
// one of the supported attribute-group suffixes.
|
||||
func IsSupportedParameterTableName(tableName string) bool {
|
||||
for _, suffix := range supportedParameterTableSuffixes {
|
||||
if strings.HasSuffix(tableName, "_"+suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package constants
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsSupportedParameterTableName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tableName string
|
||||
want bool
|
||||
}{
|
||||
{name: "bay table is excluded", tableName: "ct_ct_demo_bay", want: false},
|
||||
{name: "model table is included", tableName: "cable_cable_demo_model", want: true},
|
||||
{name: "base extend table is included", tableName: "cable_cable_demo_base_extend", want: true},
|
||||
{name: "suffix must start at separator", tableName: "cable_cable_demomodel", want: false},
|
||||
{name: "empty table name", tableName: "", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsSupportedParameterTableName(tt.tableName); got != tt.want {
|
||||
t.Fatalf("IsSupportedParameterTableName(%q) = %v, want %v", tt.tableName, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ package constants
|
|||
const (
|
||||
// DefaultScore define the default score for redissearch suggestion
|
||||
DefaultScore = 1.0
|
||||
// ComponentConfigKey define component config token used at token6
|
||||
ComponentConfigKey = "component"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -44,9 +42,6 @@ const (
|
|||
|
||||
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
|
||||
RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys"
|
||||
|
||||
// RedisSpecCompNSPathMeasSetKey define redis set key which store all measurement tag keys under specific component nspath
|
||||
RedisSpecCompNSPathMeasSetKey = "%s_nspath_measurement_tag_keys"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -12,6 +12,29 @@ const (
|
|||
SubUpdateAction string = "update"
|
||||
)
|
||||
|
||||
// 定义状态常量
|
||||
// TODO 从4位格式修改为5位格式
|
||||
const (
|
||||
// SubSuccessCode define subscription success code
|
||||
SubSuccessCode = "1001"
|
||||
// SubFailedCode define subscription failed code
|
||||
SubFailedCode = "1002"
|
||||
// RTDSuccessCode define real time data return success code
|
||||
RTDSuccessCode = "1003"
|
||||
// RTDFailedCode define real time data return failed code
|
||||
RTDFailedCode = "1004"
|
||||
// CancelSubSuccessCode define cancel subscription success code
|
||||
CancelSubSuccessCode = "1005"
|
||||
// CancelSubFailedCode define cancel subscription failed code
|
||||
CancelSubFailedCode = "1006"
|
||||
// SubRepeatCode define subscription repeat code
|
||||
SubRepeatCode = "1007"
|
||||
// UpdateSubSuccessCode define update subscription success code
|
||||
UpdateSubSuccessCode = "1008"
|
||||
// UpdateSubFailedCode define update subscription failed code
|
||||
UpdateSubFailedCode = "1009"
|
||||
)
|
||||
|
||||
const (
|
||||
// SysCtrlPrefix define to indicates the prefix for all system control directives,facilitating unified parsing within the sendDataStream goroutine
|
||||
SysCtrlPrefix = "SYS_CTRL_"
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
// Package constants defines task-related constants for the async task system
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// Task priority levels
|
||||
const (
|
||||
// TaskPriorityDefault is the default priority level for tasks
|
||||
TaskPriorityDefault = 5
|
||||
// TaskPriorityHigh represents high priority tasks
|
||||
TaskPriorityHigh = 10
|
||||
// TaskPriorityLow represents low priority tasks
|
||||
TaskPriorityLow = 1
|
||||
)
|
||||
|
||||
// Task queue configuration
|
||||
const (
|
||||
// TaskExchangeName is the name of the exchange for task routing
|
||||
TaskExchangeName = "modelrt.tasks.exchange"
|
||||
// TaskQueueName is the name of the main task queue
|
||||
TaskQueueName = "modelrt.tasks.queue"
|
||||
// TaskRoutingKey is the routing key for task messages
|
||||
TaskRoutingKey = "modelrt.task"
|
||||
)
|
||||
|
||||
// Task message settings
|
||||
const (
|
||||
// TaskMaxPriority is the maximum priority level for tasks (0-10)
|
||||
TaskMaxPriority = 10
|
||||
// TaskDefaultMessageTTL is the default time-to-live for task messages (24 hours)
|
||||
TaskDefaultMessageTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Task retry settings
|
||||
const (
|
||||
// TaskRetryMaxDefault is the default maximum number of retry attempts
|
||||
TaskRetryMaxDefault = 3
|
||||
// TaskRetryInitialDelayDefault is the default initial delay for exponential backoff
|
||||
TaskRetryInitialDelayDefault = 1 * time.Second
|
||||
// TaskRetryMaxDelayDefault is the default maximum delay for exponential backoff
|
||||
TaskRetryMaxDelayDefault = 5 * time.Minute
|
||||
// TaskRetryRandomFactorDefault is the default random factor for jitter (10%)
|
||||
TaskRetryRandomFactorDefault = 0.1
|
||||
// TaskRetryFixedDelayDefault is the default delay for fixed retry strategy
|
||||
TaskRetryFixedDelayDefault = 5 * time.Second
|
||||
)
|
||||
|
||||
// Test task settings
|
||||
const (
|
||||
// TestTaskSleepDurationDefault is the default sleep duration for test tasks (60 seconds)
|
||||
TestTaskSleepDurationDefault = 60
|
||||
// TestTaskSleepDurationMax is the maximum allowed sleep duration for test tasks (1 hour)
|
||||
TestTaskSleepDurationMax = 3600
|
||||
)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
const (
|
||||
// TIBreachTriggerType define out of bounds type constant
|
||||
TIBreachTriggerType = "trigger"
|
||||
)
|
||||
|
||||
const (
|
||||
// TelemetryUpLimit define telemetry upper limit
|
||||
TelemetryUpLimit = "up"
|
||||
// TelemetryUpUpLimit define telemetry upper upper limit
|
||||
TelemetryUpUpLimit = "upup"
|
||||
|
||||
// TelemetryDownLimit define telemetry limit
|
||||
TelemetryDownLimit = "down"
|
||||
// TelemetryDownDownLimit define telemetry lower lower limit
|
||||
TelemetryDownDownLimit = "downdown"
|
||||
)
|
||||
|
||||
const (
|
||||
// TelesignalRaising define telesignal raising edge
|
||||
TelesignalRaising = "raising"
|
||||
// TelesignalFalling define telesignal falling edge
|
||||
TelesignalFalling = "falling"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinBreachCount define min breach count of real time data
|
||||
MinBreachCount = 10
|
||||
)
|
||||
|
|
@ -1,21 +1,9 @@
|
|||
// Package constants define constant variable
|
||||
package constants
|
||||
|
||||
// Internal context keys for trace values set by StartTrace middleware.
|
||||
// These are gin/stdlib context keys only — actual W3C header propagation
|
||||
// (traceparent / tracestate) is handled automatically by the OTel propagator.
|
||||
// Assuming the B3 specification
|
||||
const (
|
||||
HeaderTraceID = "trace-id"
|
||||
HeaderSpanID = "span-id"
|
||||
HeaderParentSpanID = "parent-span-id"
|
||||
)
|
||||
|
||||
// traceCtxKey is an unexported type for context keys to avoid collisions with other packages.
|
||||
type traceCtxKey string
|
||||
|
||||
// Typed context keys for trace values — use these with context.WithValue / ctx.Value.
|
||||
var (
|
||||
CtxKeyTraceID = traceCtxKey(HeaderTraceID)
|
||||
CtxKeySpanID = traceCtxKey(HeaderSpanID)
|
||||
CtxKeyParentSpanID = traceCtxKey(HeaderParentSpanID)
|
||||
HeaderTraceID = "X-B3-TraceId"
|
||||
HeaderSpanID = "X-B3-SpanId"
|
||||
HeaderParentSpanID = "X-B3-ParentSpanId"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,228 +0,0 @@
|
|||
// Package database define database operation functions
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UpdateTaskStarted updates task start time and status to running
|
||||
func UpdateTaskStarted(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, startedAt int64) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"status": orm.AsyncTaskStatusRunning,
|
||||
"started_at": startedAt,
|
||||
})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskRetryInfo updates task retry information
|
||||
func UpdateTaskRetryInfo(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, retryCount int, nextRetryTime int64) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
updateData := map[string]any{
|
||||
"retry_count": retryCount,
|
||||
}
|
||||
if nextRetryTime <= 0 {
|
||||
updateData["next_retry_time"] = nil
|
||||
} else {
|
||||
updateData["next_retry_time"] = nextRetryTime
|
||||
}
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(updateData)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskErrorInfo updates task error information
|
||||
func UpdateTaskErrorInfo(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, errorMsg, stackTrace string) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"failure_reason": errorMsg,
|
||||
"stack_trace": stackTrace,
|
||||
"status": orm.AsyncTaskStatusFailed,
|
||||
})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskExecutionTime updates task execution time
|
||||
func UpdateTaskExecutionTime(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, executionTime int64) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("execution_time", executionTime)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskWorkerID updates the worker ID that is processing the task
|
||||
func UpdateTaskWorkerID(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, workerID string) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("worker_id", workerID)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskPriority updates task priority
|
||||
func UpdateTaskPriority(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, priority int) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("priority", priority)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskQueueName updates task queue name
|
||||
func UpdateTaskQueueName(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, queueName string) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("queue_name", queueName)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskCreatedBy updates task creator information
|
||||
func UpdateTaskCreatedBy(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, createdBy string) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("created_by", createdBy)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateTaskResultWithMetrics updates task result with execution metrics
|
||||
func UpdateTaskResultWithMetrics(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, executionTime int64, memoryUsage *int64, cpuUsage *float64, retryCount int, completedAt int64) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTaskResult{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"execution_time": executionTime,
|
||||
"memory_usage": memoryUsage,
|
||||
"cpu_usage": cpuUsage,
|
||||
"retry_count": retryCount,
|
||||
"completed_at": completedAt,
|
||||
})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// GetTasksForRetry retrieves tasks that are due for retry
|
||||
func GetTasksForRetry(ctx context.Context, tx *gorm.DB, limit int) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
now := time.Now().Unix()
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("status = ? AND next_retry_time IS NOT NULL AND next_retry_time <= ?", orm.AsyncTaskStatusFailed, now).
|
||||
Order("next_retry_time ASC").
|
||||
Limit(limit).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetTasksByPriority retrieves tasks by priority order
|
||||
func GetTasksByPriority(ctx context.Context, tx *gorm.DB, status orm.AsyncTaskStatus, limit int) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("status = ?", status).
|
||||
Order("priority DESC, created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetTasksByWorkerID retrieves tasks being processed by a specific worker
|
||||
func GetTasksByWorkerID(ctx context.Context, tx *gorm.DB, workerID string) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("worker_id = ? AND status = ?", workerID, orm.AsyncTaskStatusRunning).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// CleanupStaleTasks marks tasks as failed if they have been running for too long
|
||||
func CleanupStaleTasks(ctx context.Context, tx *gorm.DB, timeoutSeconds int64) (int64, error) {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
threshold := time.Now().Unix() - timeoutSeconds
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("status = ? AND started_at IS NOT NULL AND started_at < ?", orm.AsyncTaskStatusRunning, threshold).
|
||||
Updates(map[string]any{
|
||||
"status": orm.AsyncTaskStatusFailed,
|
||||
"failure_reason": "task timeout",
|
||||
"finished_at": time.Now().Unix(),
|
||||
})
|
||||
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
// Package database define database operation functions
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// CreateAsyncTask creates a new async task in the database
|
||||
func CreateAsyncTask(ctx context.Context, tx *gorm.DB, taskType orm.AsyncTaskType, params orm.JSONMap) (*orm.AsyncTask, error) {
|
||||
taskID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := &orm.AsyncTask{
|
||||
TaskID: taskID,
|
||||
TaskType: taskType,
|
||||
Status: orm.AsyncTaskStatusSubmitted,
|
||||
Params: params,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).Create(task)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetAsyncTaskByID retrieves an async task by its ID
|
||||
func GetAsyncTaskByID(ctx context.Context, tx *gorm.DB, taskID uuid.UUID) (*orm.AsyncTask, error) {
|
||||
var task orm.AsyncTask
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("task_id = ?", taskID).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
First(&task)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// GetAsyncTasksByIDs retrieves multiple async tasks by their IDs
|
||||
func GetAsyncTasksByIDs(ctx context.Context, tx *gorm.DB, taskIDs []uuid.UUID) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
if len(taskIDs) == 0 {
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("task_id IN ?", taskIDs).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// UpdateAsyncTaskStatus updates the status of an async task
|
||||
func UpdateAsyncTaskStatus(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, status orm.AsyncTaskStatus) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("status", status)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// UpdateAsyncTaskProgress updates the progress of an async task
|
||||
func UpdateAsyncTaskProgress(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, progress int) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Update("progress", progress)
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// CompleteAsyncTask marks an async task as completed with timestamp
|
||||
func CompleteAsyncTask(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, timestamp int64) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"status": orm.AsyncTaskStatusCompleted,
|
||||
"finished_at": timestamp,
|
||||
"progress": 100,
|
||||
})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// FailAsyncTask marks an async task as failed with timestamp
|
||||
func FailAsyncTask(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, timestamp int64) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTask{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"status": orm.AsyncTaskStatusFailed,
|
||||
"finished_at": timestamp,
|
||||
})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// CreateAsyncTaskResult creates a result record for an async task
|
||||
func CreateAsyncTaskResult(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, result orm.JSONMap) error {
|
||||
taskResult := &orm.AsyncTaskResult{
|
||||
TaskID: taskID,
|
||||
Result: result,
|
||||
}
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resultOp := tx.WithContext(cancelCtx).Create(taskResult)
|
||||
return resultOp.Error
|
||||
}
|
||||
|
||||
// UpdateAsyncTaskResultWithError upserts a task result with error information.
|
||||
func UpdateAsyncTaskResultWithError(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, code int, message string, detail orm.JSONMap) error {
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := tx.WithContext(cancelCtx).
|
||||
Where("task_id = ?", taskID).
|
||||
FirstOrCreate(&orm.AsyncTaskResult{TaskID: taskID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTaskResult{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"error_code": code,
|
||||
"error_message": message,
|
||||
"error_detail": detail,
|
||||
"result": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// UpdateAsyncTaskResultWithSuccess updates a task result with success information
|
||||
func UpdateAsyncTaskResultWithSuccess(ctx context.Context, tx *gorm.DB, taskID uuid.UUID, result orm.JSONMap) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First try to update existing record, if not found create new one
|
||||
existingResult := tx.WithContext(cancelCtx).
|
||||
Where("task_id = ?", taskID).
|
||||
FirstOrCreate(&orm.AsyncTaskResult{TaskID: taskID})
|
||||
|
||||
if existingResult.Error != nil {
|
||||
return existingResult.Error
|
||||
}
|
||||
|
||||
// Update with success information
|
||||
updateResult := tx.WithContext(cancelCtx).
|
||||
Model(&orm.AsyncTaskResult{}).
|
||||
Where("task_id = ?", taskID).
|
||||
Updates(map[string]any{
|
||||
"result": result,
|
||||
"error_code": nil,
|
||||
"error_message": nil,
|
||||
"error_detail": nil,
|
||||
})
|
||||
|
||||
return updateResult.Error
|
||||
}
|
||||
|
||||
// GetAsyncTaskResult retrieves the result of an async task
|
||||
func GetAsyncTaskResult(ctx context.Context, tx *gorm.DB, taskID uuid.UUID) (*orm.AsyncTaskResult, error) {
|
||||
var taskResult orm.AsyncTaskResult
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("task_id = ?", taskID).
|
||||
First(&taskResult)
|
||||
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return &taskResult, nil
|
||||
}
|
||||
|
||||
// GetAsyncTaskResults retrieves multiple task results by task IDs
|
||||
func GetAsyncTaskResults(ctx context.Context, tx *gorm.DB, taskIDs []uuid.UUID) ([]orm.AsyncTaskResult, error) {
|
||||
var taskResults []orm.AsyncTaskResult
|
||||
|
||||
if len(taskIDs) == 0 {
|
||||
return taskResults, nil
|
||||
}
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("task_id IN ?", taskIDs).
|
||||
Find(&taskResults)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return taskResults, nil
|
||||
}
|
||||
|
||||
// GetPendingTasks retrieves pending tasks (submitted but not yet running/completed)
|
||||
func GetPendingTasks(ctx context.Context, tx *gorm.DB, limit int) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("status = ?", orm.AsyncTaskStatusSubmitted).
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// GetTasksByStatus retrieves tasks by status
|
||||
func GetTasksByStatus(ctx context.Context, tx *gorm.DB, status orm.AsyncTaskStatus, limit int) ([]orm.AsyncTask, error) {
|
||||
var tasks []orm.AsyncTask
|
||||
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("status = ?", status).
|
||||
Order("created_at ASC").
|
||||
Limit(limit).
|
||||
Find(&tasks)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// DeleteOldTasks deletes tasks older than the specified timestamp
|
||||
func DeleteOldTasks(ctx context.Context, tx *gorm.DB, olderThan int64) error {
|
||||
// ctx timeout judgment
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First delete task results
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("task_id IN (SELECT task_id FROM async_task WHERE created_at < ?)", olderThan).
|
||||
Delete(&orm.AsyncTaskResult{})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// Then delete tasks
|
||||
result = tx.WithContext(cancelCtx).
|
||||
Where("created_at < ?", olderThan).
|
||||
Delete(&orm.AsyncTask{})
|
||||
|
||||
return result.Error
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ func CreateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
|
|||
Name: componentInfo.Name,
|
||||
Context: componentInfo.Context,
|
||||
Op: componentInfo.Op,
|
||||
TS: time.Now(),
|
||||
Ts: time.Now(),
|
||||
}
|
||||
|
||||
result := tx.WithContext(cancelCtx).Create(&component)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func CreateMeasurement(ctx context.Context, tx *gorm.DB, measurementInfo network
|
|||
BayUUID: globalUUID,
|
||||
ComponentUUID: globalUUID,
|
||||
Op: -1,
|
||||
TS: time.Now(),
|
||||
Ts: time.Now(),
|
||||
}
|
||||
|
||||
result := tx.WithContext(cancelCtx).Create(&measurement)
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ func FillingLongTokenModel(ctx context.Context, tx *gorm.DB, identModel *model.L
|
|||
func ParseDataIdentifierToken(ctx context.Context, tx *gorm.DB, identToken string) (model.IndentityTokenModelInterface, error) {
|
||||
identSlice := strings.Split(identToken, ".")
|
||||
identSliceLen := len(identSlice)
|
||||
switch identSliceLen {
|
||||
case 4:
|
||||
if identSliceLen == 4 {
|
||||
// token1.token2.token3.token4.token7
|
||||
shortIndentModel := &model.ShortIdentityTokenModel{
|
||||
GridTag: identSlice[0],
|
||||
|
|
@ -68,7 +67,7 @@ func ParseDataIdentifierToken(ctx context.Context, tx *gorm.DB, identToken strin
|
|||
return nil, err
|
||||
}
|
||||
return shortIndentModel, nil
|
||||
case 7:
|
||||
} else if identSliceLen == 7 {
|
||||
// token1.token2.token3.token4.token5.token6.token7
|
||||
longIndentModel := &model.LongIdentityTokenModel{
|
||||
GridTag: identSlice[0],
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ func ParseAttrToken(ctx context.Context, tx *gorm.DB, attrToken, clientToken str
|
|||
|
||||
attrSlice := strings.Split(attrToken, ".")
|
||||
attrLen := len(attrSlice)
|
||||
switch attrLen {
|
||||
case 4:
|
||||
if attrLen == 4 {
|
||||
short := &model.ShortAttrInfo{
|
||||
AttrGroupName: attrSlice[2],
|
||||
AttrKey: attrSlice[3],
|
||||
|
|
@ -36,7 +35,7 @@ func ParseAttrToken(ctx context.Context, tx *gorm.DB, attrToken, clientToken str
|
|||
}
|
||||
short.AttrValue = attrValue
|
||||
return short, nil
|
||||
case 7:
|
||||
} else if attrLen == 7 {
|
||||
long := &model.LongAttrInfo{
|
||||
AttrGroupName: attrSlice[5],
|
||||
AttrKey: attrSlice[6],
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"modelRT/logger"
|
||||
"modelRT/orm"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -15,11 +15,15 @@ import (
|
|||
var (
|
||||
postgresOnce sync.Once
|
||||
_globalPostgresClient *gorm.DB
|
||||
_globalPostgresMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetPostgresDBClient returns the global PostgresDB client.It's safe for concurrent use.
|
||||
func GetPostgresDBClient() *gorm.DB {
|
||||
return _globalPostgresClient
|
||||
_globalPostgresMu.RLock()
|
||||
client := _globalPostgresClient
|
||||
_globalPostgresMu.RUnlock()
|
||||
return client
|
||||
}
|
||||
|
||||
// InitPostgresDBInstance return instance of PostgresDB client
|
||||
|
|
@ -32,19 +36,11 @@ func InitPostgresDBInstance(ctx context.Context, PostgresDBURI string) *gorm.DB
|
|||
|
||||
// initPostgresDBClient return successfully initialized PostgresDB client
|
||||
func initPostgresDBClient(ctx context.Context, PostgresDBURI string) *gorm.DB {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
db, err := gorm.Open(postgres.Open(PostgresDBURI), &gorm.Config{Logger: logger.NewGormLogger()})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Auto migrate async task tables
|
||||
err = db.WithContext(ctx).AutoMigrate(
|
||||
&orm.AsyncTask{},
|
||||
&orm.AsyncTaskResult{},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
// Package database define database operation functions
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"modelRT/logger"
|
||||
"modelRT/orm"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// QueryBayByUUID returns the Bay record matching bayUUID.
|
||||
func QueryBayByUUID(ctx context.Context, tx *gorm.DB, bayUUID uuid.UUID) (*orm.Bay, error) {
|
||||
var bay orm.Bay
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("bay_uuid = ?", bayUUID).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
First(&bay)
|
||||
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &bay, nil
|
||||
}
|
||||
|
||||
// QueryBaysByUUIDs returns Bay records matching the given UUIDs in a single query.
|
||||
// The returned slice preserves database order; unmatched UUIDs are silently omitted.
|
||||
func QueryBaysByUUIDs(ctx context.Context, tx *gorm.DB, bayUUIDs []uuid.UUID) ([]orm.Bay, error) {
|
||||
if len(bayUUIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var bays []orm.Bay
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("bay_uuid IN ?", bayUUIDs).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Find(&bays)
|
||||
|
||||
if result.Error != nil {
|
||||
logger.Error(ctx, "query bays by uuids failed", "error", result.Error)
|
||||
return nil, result.Error
|
||||
}
|
||||
return bays, nil
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// QueryBayDevColumnNames returns the bay table columns exposed as token7
|
||||
// candidates under token6=bay.
|
||||
func QueryBayDevColumnNames(ctx context.Context, db *gorm.DB) ([]string, error) {
|
||||
columnTypes, err := db.WithContext(ctx).Migrator().ColumnTypes((&orm.Bay{}).TableName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columnNames := make([]string, 0, len(columnTypes))
|
||||
for _, columnType := range columnTypes {
|
||||
columnName := columnType.Name()
|
||||
if strings.HasPrefix(columnName, "dev_") {
|
||||
columnNames = append(columnNames, columnName)
|
||||
}
|
||||
}
|
||||
return columnNames, nil
|
||||
}
|
||||
|
|
@ -148,39 +148,6 @@ func QueryLongIdentModelInfoByToken(ctx context.Context, tx *gorm.DB, measTag st
|
|||
return &resultComp, &meauserment, nil
|
||||
}
|
||||
|
||||
// QueryComponentsInServiceByUUIDs returns a map of global_uuid → in_service for the
|
||||
// given UUIDs. Only global_uuid and in_service columns are selected for efficiency.
|
||||
func QueryComponentsInServiceByUUIDs(ctx context.Context, tx *gorm.DB, uuids []uuid.UUID) (map[uuid.UUID]bool, error) {
|
||||
if len(uuids) == 0 {
|
||||
return make(map[uuid.UUID]bool), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
GlobalUUID uuid.UUID `gorm:"column:global_uuid"`
|
||||
InService bool `gorm:"column:in_service"`
|
||||
}
|
||||
|
||||
var rows []row
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Model(&orm.Component{}).
|
||||
Select("global_uuid, in_service").
|
||||
Where("global_uuid IN ?", uuids).
|
||||
Scan(&rows)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
m := make(map[uuid.UUID]bool, len(rows))
|
||||
for _, r := range rows {
|
||||
m[r.GlobalUUID] = r.InService
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// QueryShortIdentModelInfoByToken define func to query short identity model info by short token
|
||||
func QueryShortIdentModelInfoByToken(ctx context.Context, tx *gorm.DB, measTag string, condition *orm.Component) (*orm.Component, *orm.Measurement, error) {
|
||||
var resultComp orm.Component
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// Package database define database operation functions
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// QueryComponentColumnNames returns all column names from the component table.
|
||||
func QueryComponentColumnNames(ctx context.Context, db *gorm.DB) ([]string, error) {
|
||||
columnTypes, err := db.WithContext(ctx).Migrator().ColumnTypes((&orm.Component{}).TableName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columnNames := make([]string, 0, len(columnTypes))
|
||||
for _, columnType := range columnTypes {
|
||||
columnName := columnType.Name()
|
||||
if columnName == "" {
|
||||
continue
|
||||
}
|
||||
columnNames = append(columnNames, columnName)
|
||||
}
|
||||
return columnNames, nil
|
||||
}
|
||||
|
|
@ -2,428 +2,109 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"modelRT/common"
|
||||
"modelRT/constants"
|
||||
"modelRT/orm"
|
||||
"modelRT/sql"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
measurementOperationsLimit = 500
|
||||
measurementOperationAppendSQL = "(array_append(operations, ?::jsonb))[GREATEST(cardinality(operations) - ? + 2, 1):]"
|
||||
)
|
||||
|
||||
// QueryMeasurementByID returns a measurement by primary key without acquiring
|
||||
// a row lock. Call QueryMeasurementByIDForUpdate for write workflows.
|
||||
func QueryMeasurementByID(ctx context.Context, db *gorm.DB, id int64) (orm.Measurement, error) {
|
||||
var measurement orm.Measurement
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
result := db.WithContext(cancelCtx).
|
||||
Where(sql.MeasurementIDWhere, id).
|
||||
Take(&measurement)
|
||||
|
||||
if result.Error != nil {
|
||||
return orm.Measurement{}, fmt.Errorf("query measurement %d: %w", id, result.Error)
|
||||
}
|
||||
return measurement, nil
|
||||
type ZoneWithParent struct {
|
||||
orm.Zone
|
||||
GridTag string `gorm:"column:grid_tag"`
|
||||
}
|
||||
|
||||
// QueryMeasurementByIDForUpdate locks a measurement row and loads only the
|
||||
// fields required by the data-object update workflow.
|
||||
func QueryMeasurementByIDForUpdate(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
|
||||
var measurement orm.Measurement
|
||||
result := tx.WithContext(ctx).
|
||||
Select("id", "mode", "data_source").
|
||||
Where(sql.MeasurementIDWhere, id).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Take(&measurement)
|
||||
if result.Error != nil {
|
||||
return orm.Measurement{}, fmt.Errorf("lock measurement %d: %w", id, result.Error)
|
||||
}
|
||||
return measurement, nil
|
||||
type StationWithParent struct {
|
||||
orm.Zone
|
||||
ZoneTag string `gorm:"column:zone_tag"`
|
||||
}
|
||||
|
||||
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
||||
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
||||
measurement, _, err := QueryMeasurementByDataObjectToken(ctx, tx, token)
|
||||
if err != nil {
|
||||
return orm.Measurement{}, err
|
||||
}
|
||||
return *measurement, nil
|
||||
}
|
||||
|
||||
// UpdateMeasurementMode stores the data-object mode representation in the
|
||||
// measurement row: false is manual mode (0), true is automatic mode (1).
|
||||
func UpdateMeasurementMode(ctx context.Context, db *gorm.DB, measurementID int64, automatic bool) error {
|
||||
mode := constants.MeasurementModeManual
|
||||
if automatic {
|
||||
mode = constants.MeasurementModeAutomatic
|
||||
}
|
||||
|
||||
result := db.WithContext(ctx).
|
||||
Model(&orm.Measurement{}).
|
||||
Where("id = ?", measurementID).
|
||||
Update("mode", mode)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("update measurement %d mode: %w", measurementID, result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("update measurement %d mode affected no rows", measurementID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMeasurementModeWithOperation changes mode and appends its audit entry
|
||||
// atomically. The operations array retains only its newest 500 entries.
|
||||
func UpdateMeasurementModeWithOperation(ctx context.Context, db *gorm.DB, measurementID int64, mode int16, timestamp time.Time) error {
|
||||
if mode != constants.MeasurementModeManual && mode != constants.MeasurementModeAutomatic {
|
||||
return fmt.Errorf("measurement mode must be 0 or 1, got %d", mode)
|
||||
}
|
||||
operation := orm.JSONMap{
|
||||
"command": mode,
|
||||
"timestamp": timestamp.UnixMilli(),
|
||||
}
|
||||
return updateMeasurementWithOperation(ctx, db, measurementID, map[string]any{"mode": mode}, operation)
|
||||
}
|
||||
|
||||
// AppendMeasurementValueOperation appends the audit result of a manual-value
|
||||
// transaction without changing other measurement columns.
|
||||
func AppendMeasurementValueOperation(ctx context.Context, db *gorm.DB, measurementID int64, transaction int, value float64, timestamp time.Time) error {
|
||||
operation := orm.JSONMap{
|
||||
"transaction": transaction,
|
||||
"value": value,
|
||||
"timestamp": timestamp.UnixMilli(),
|
||||
}
|
||||
return updateMeasurementWithOperation(ctx, db, measurementID, nil, operation)
|
||||
}
|
||||
|
||||
func updateMeasurementWithOperation(ctx context.Context, db *gorm.DB, measurementID int64, updates map[string]any, operation orm.JSONMap) error {
|
||||
encodedOperation, err := json.Marshal(operation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode measurement %d operation: %w", measurementID, err)
|
||||
}
|
||||
|
||||
operationExpression := gorm.Expr(
|
||||
measurementOperationAppendSQL,
|
||||
string(encodedOperation),
|
||||
measurementOperationsLimit,
|
||||
)
|
||||
if updates == nil {
|
||||
updates = make(map[string]any, 1)
|
||||
}
|
||||
updates["operations"] = operationExpression
|
||||
|
||||
result := db.WithContext(ctx).
|
||||
Model(&orm.Measurement{}).
|
||||
Where("id = ?", measurementID).
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("update measurement %d operation: %w", measurementID, result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("update measurement %d operation affected no rows", measurementID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateMeasurementToken checks whether token uniquely identifies an existing
|
||||
// measurement through the measurement, component, bay, station, zone, and grid
|
||||
// relationships. Supported formats are token1-token7, token4-token7, and
|
||||
// token4.token7.
|
||||
func ValidateMeasurementToken(ctx context.Context, db *gorm.DB, token string) error {
|
||||
query, args, err := buildMeasurementTokenValidationQuery(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&count).Error; err != nil {
|
||||
return fmt.Errorf("query measurement token %q: %w", token, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case count == 0:
|
||||
return fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
||||
case count > 1:
|
||||
return fmt.Errorf("%w: %q matched %d records", common.ErrAmbiguousMeasurementToken, token, count)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// QueryMeasurementByDataObjectToken validates token and returns the existing
|
||||
// measurement and its owning component for attribute response construction.
|
||||
func QueryMeasurementByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*orm.Measurement, *orm.Component, error) {
|
||||
validationQuery, args, err := buildMeasurementTokenValidationQuery(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
query := buildMeasurementRowsQuery(validationQuery)
|
||||
var rows []orm.Measurement
|
||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("query measurement token %q: %w", token, err)
|
||||
}
|
||||
|
||||
switch len(rows) {
|
||||
case 0:
|
||||
return nil, nil, fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
||||
case 1:
|
||||
// Continue by loading the owning component.
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("%w: %q matched more than one record", common.ErrAmbiguousMeasurementToken, token)
|
||||
}
|
||||
|
||||
var component orm.Component
|
||||
result := db.WithContext(ctx).
|
||||
Raw(compactMeasurementSQL(sql.MeasurementComponentByUUID), rows[0].ComponentUUID).
|
||||
Scan(&component)
|
||||
if result.Error != nil {
|
||||
return nil, nil, fmt.Errorf("query component for measurement token %q: %w", token, result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, nil, fmt.Errorf("%w: component for %q", common.ErrMeasurementTokenNotFound, token)
|
||||
}
|
||||
|
||||
return &rows[0], &component, nil
|
||||
}
|
||||
|
||||
func buildMeasurementRowsQuery(validationQuery string) string {
|
||||
measurementQuery := strings.Replace(
|
||||
validationQuery,
|
||||
sql.MeasurementCountSelect,
|
||||
sql.MeasurementRowsSelect,
|
||||
1,
|
||||
)
|
||||
return compactMeasurementSQL(strings.Join([]string{measurementQuery, sql.MeasurementLimitTwo}, "\n"))
|
||||
}
|
||||
|
||||
func compactMeasurementSQL(statement string) string {
|
||||
return strings.Join(strings.Fields(statement), " ")
|
||||
}
|
||||
|
||||
func buildMeasurementTokenValidationQuery(token string) (string, []any, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
return "", nil, fmt.Errorf("%w %q: token segment cannot be empty", common.ErrInvalidMeasurementToken, token)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(parts) {
|
||||
case 7:
|
||||
if parts[5] != "bay" {
|
||||
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
||||
}
|
||||
query := compactMeasurementSQL(strings.Join([]string{
|
||||
sql.MeasurementTokenValidationQueryBase,
|
||||
sql.MeasurementSevenPartTokenWhere,
|
||||
}, "\n"))
|
||||
return query, []any{parts[0], parts[1], parts[2], parts[3], parts[4], parts[6]}, nil
|
||||
case 4:
|
||||
if parts[2] != "bay" {
|
||||
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
||||
}
|
||||
query := compactMeasurementSQL(strings.Join([]string{
|
||||
sql.MeasurementTokenValidationQueryBase,
|
||||
sql.MeasurementFourPartTokenWhere,
|
||||
}, "\n"))
|
||||
return query, []any{parts[0], parts[1], parts[3]}, nil
|
||||
case 2:
|
||||
query := compactMeasurementSQL(strings.Join([]string{
|
||||
sql.MeasurementTokenValidationQueryBase,
|
||||
sql.MeasurementTwoPartTokenWhere,
|
||||
}, "\n"))
|
||||
return query, []any{parts[0], parts[1]}, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf("%w %q: expected 2, 4, or 7 segments, got %d", common.ErrInvalidMeasurementToken, token, len(parts))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllMeasurements define func to query all measurement info from postgresDB
|
||||
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
||||
var measurements []orm.Measurement
|
||||
|
||||
// ctx超时判断
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return measurements, nil
|
||||
}
|
||||
|
||||
// GetFullMeasurementSet queries all hierarchy tags required to build
|
||||
// measurement recommendations.
|
||||
func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSet, error) {
|
||||
func GetFullMeasurementSet(db *gorm.DB) (*orm.MeasurementSet, error) {
|
||||
mSet := &orm.MeasurementSet{
|
||||
GridToZoneTags: make(map[string][]string),
|
||||
ZoneToStationTags: make(map[string][]string),
|
||||
StationToCompNSPaths: make(map[string][]string),
|
||||
CompNSPathToCompTags: make(map[string][]string),
|
||||
CompTagToMeasTags: make(map[string][]string),
|
||||
CompNSPathToMeasTags: make(map[string][]string),
|
||||
}
|
||||
|
||||
g, gctx := errgroup.WithContext(ctx)
|
||||
db = db.WithContext(gctx)
|
||||
var bayLinkedCompTags []string
|
||||
var bayDevColumnNames []string
|
||||
|
||||
g.Go(func() error {
|
||||
var linkedComponents []struct {
|
||||
CompTag string `gorm:"column:comp_tag"`
|
||||
}
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementBayLinkedComponentTags)).Scan(&linkedComponents).Error; err != nil {
|
||||
return fmt.Errorf("query bay-linked components: %w", err)
|
||||
}
|
||||
bayLinkedCompTags = make([]string, 0, len(linkedComponents))
|
||||
for _, component := range linkedComponents {
|
||||
bayLinkedCompTags = append(bayLinkedCompTags, component.CompTag)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
bayDevColumnNames, err = QueryBayDevColumnNames(gctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query bay dev columns: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var grids []orm.Grid
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementGridTags)).Scan(&grids).Error; err != nil {
|
||||
return fmt.Errorf("query grids: %w", err)
|
||||
}
|
||||
for _, grid := range grids {
|
||||
if grid.TAGNAME != "" {
|
||||
mSet.AllGridTags = append(mSet.AllGridTags, grid.TAGNAME)
|
||||
var grids []orm.Grid
|
||||
if err := db.Table("grid").Select("tagname").Scan(&grids).Error; err == nil {
|
||||
for _, g := range grids {
|
||||
if g.TAGNAME != "" {
|
||||
mSet.AllGridTags = append(mSet.AllGridTags, g.TAGNAME)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var zones []struct {
|
||||
orm.Zone
|
||||
GridTag string `gorm:"column:grid_tag"`
|
||||
}
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementZoneHierarchy)).Scan(&zones).Error; err != nil {
|
||||
return fmt.Errorf("query zones: %w", err)
|
||||
}
|
||||
for _, zone := range zones {
|
||||
mSet.AllZoneTags = append(mSet.AllZoneTags, zone.TAGNAME)
|
||||
if zone.GridTag != "" {
|
||||
mSet.GridToZoneTags[zone.GridTag] = append(mSet.GridToZoneTags[zone.GridTag], zone.TAGNAME)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var stations []struct {
|
||||
orm.Station
|
||||
ZoneTag string `gorm:"column:zone_tag"`
|
||||
}
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementStationHierarchy)).Scan(&stations).Error; err != nil {
|
||||
return fmt.Errorf("query stations: %w", err)
|
||||
}
|
||||
for _, station := range stations {
|
||||
mSet.AllStationTags = append(mSet.AllStationTags, station.TAGNAME)
|
||||
if station.ZoneTag != "" {
|
||||
mSet.ZoneToStationTags[station.ZoneTag] = append(mSet.ZoneToStationTags[station.ZoneTag], station.TAGNAME)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var components []struct {
|
||||
orm.Component
|
||||
StationTag string `gorm:"column:station_tag"`
|
||||
}
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementComponentHierarchy)).Scan(&components).Error; err != nil {
|
||||
return fmt.Errorf("query components: %w", err)
|
||||
}
|
||||
for _, component := range components {
|
||||
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, component.NSPath)
|
||||
mSet.AllCompTags = append(mSet.AllCompTags, component.Tag)
|
||||
if component.StationTag != "" {
|
||||
mSet.StationToCompNSPaths[component.StationTag] = append(
|
||||
mSet.StationToCompNSPaths[component.StationTag],
|
||||
component.NSPath,
|
||||
)
|
||||
}
|
||||
if component.NSPath != "" {
|
||||
mSet.CompNSPathToCompTags[component.NSPath] = append(
|
||||
mSet.CompNSPathToCompTags[component.NSPath],
|
||||
component.Tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var measurements []struct {
|
||||
orm.Measurement
|
||||
CompTag string `gorm:"column:comp_tag"`
|
||||
CompNSPath string `gorm:"column:comp_nspath"`
|
||||
BayTag string `gorm:"column:bay_tag"`
|
||||
}
|
||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementTagHierarchy)).Scan(&measurements).Error; err != nil {
|
||||
return fmt.Errorf("query measurements: %w", err)
|
||||
}
|
||||
for _, measurement := range measurements {
|
||||
mSet.AllMeasTags = append(mSet.AllMeasTags, measurement.Tag)
|
||||
if measurement.CompTag != "" {
|
||||
mSet.CompTagToMeasTags[measurement.CompTag] = append(
|
||||
mSet.CompTagToMeasTags[measurement.CompTag],
|
||||
measurement.Tag,
|
||||
)
|
||||
}
|
||||
if measurement.CompNSPath != "" && measurement.CompNSPath == measurement.BayTag {
|
||||
mSet.CompNSPathToMeasTags[measurement.CompNSPath] = append(mSet.CompNSPathToMeasTags[measurement.CompNSPath], measurement.Tag)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appendBayDevCandidates(mSet, bayLinkedCompTags, bayDevColumnNames)
|
||||
var zones []struct {
|
||||
orm.Zone
|
||||
GridTag string `gorm:"column:grid_tag"`
|
||||
}
|
||||
if err := db.Table("zone").
|
||||
Select("zone.*, grid.tagname as grid_tag").
|
||||
Joins("left join grid on zone.grid_id = grid.id").
|
||||
Scan(&zones).Error; err == nil {
|
||||
for _, z := range zones {
|
||||
mSet.AllZoneTags = append(mSet.AllZoneTags, z.TAGNAME)
|
||||
if z.GridTag != "" {
|
||||
mSet.GridToZoneTags[z.GridTag] = append(mSet.GridToZoneTags[z.GridTag], z.TAGNAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stations []struct {
|
||||
orm.Station
|
||||
ZoneTag string `gorm:"column:zone_tag"`
|
||||
}
|
||||
if err := db.Table("station").
|
||||
Select("station.*, zone.tagname as zone_tag").
|
||||
Joins("left join zone on station.zone_id = zone.id").
|
||||
Scan(&stations).Error; err == nil {
|
||||
for _, s := range stations {
|
||||
mSet.AllStationTags = append(mSet.AllStationTags, s.TAGNAME)
|
||||
if s.ZoneTag != "" {
|
||||
mSet.ZoneToStationTags[s.ZoneTag] = append(mSet.ZoneToStationTags[s.ZoneTag], s.TAGNAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var comps []struct {
|
||||
orm.Component
|
||||
StationTag string `gorm:"column:station_tag"`
|
||||
}
|
||||
if err := db.Table("component").
|
||||
Select("component.*, station.tagname as station_tag").
|
||||
Joins("left join station on component.station_id = station.id").
|
||||
Scan(&comps).Error; err == nil {
|
||||
for _, c := range comps {
|
||||
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, c.NSPath)
|
||||
mSet.AllCompTags = append(mSet.AllCompTags, c.Tag)
|
||||
|
||||
if c.StationTag != "" {
|
||||
mSet.StationToCompNSPaths[c.StationTag] = append(mSet.StationToCompNSPaths[c.StationTag], c.NSPath)
|
||||
}
|
||||
|
||||
if c.NSPath != "" {
|
||||
mSet.CompNSPathToCompTags[c.NSPath] = append(mSet.CompNSPathToCompTags[c.NSPath], c.Tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mSet.AllConfigTags = append(mSet.AllConfigTags, "bay")
|
||||
|
||||
var measurements []struct {
|
||||
orm.Measurement
|
||||
CompTag string `gorm:"column:comp_tag"`
|
||||
}
|
||||
if err := db.Table("measurement").
|
||||
Select("measurement.*, component.tag as comp_tag").
|
||||
Joins("left join component on measurement.component_uuid = component.global_uuid").
|
||||
Scan(&measurements).Error; err == nil {
|
||||
for _, m := range measurements {
|
||||
mSet.AllMeasTags = append(mSet.AllMeasTags, m.Tag)
|
||||
if m.CompTag != "" {
|
||||
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mSet, nil
|
||||
}
|
||||
|
||||
func appendBayDevCandidates(mSet *orm.MeasurementSet, bayLinkedCompTags, bayDevColumnNames []string) {
|
||||
if mSet == nil || len(bayLinkedCompTags) == 0 || len(bayDevColumnNames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mSet.AllMeasTags = append(mSet.AllMeasTags, bayDevColumnNames...)
|
||||
for _, compTag := range bayLinkedCompTags {
|
||||
mSet.CompTagToMeasTags[compTag] = append(
|
||||
mSet.CompTagToMeasTags[compTag],
|
||||
bayDevColumnNames...,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppendBayDevCandidatesRequiresBayLinkedComponent(t *testing.T) {
|
||||
measurementSet := &orm.MeasurementSet{
|
||||
AllMeasTags: []string{"current"},
|
||||
CompTagToMeasTags: map[string][]string{
|
||||
"linked-component": {"current"},
|
||||
"unlinked-component": {"voltage"},
|
||||
},
|
||||
}
|
||||
|
||||
appendBayDevCandidates(
|
||||
measurementSet,
|
||||
[]string{"linked-component"},
|
||||
[]string{"dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||
)
|
||||
|
||||
require.Equal(t,
|
||||
[]string{"current", "dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||
measurementSet.AllMeasTags,
|
||||
)
|
||||
require.Equal(t,
|
||||
[]string{"current", "dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||
measurementSet.CompTagToMeasTags["linked-component"],
|
||||
)
|
||||
require.Equal(t,
|
||||
[]string{"voltage"},
|
||||
measurementSet.CompTagToMeasTags["unlinked-component"],
|
||||
)
|
||||
}
|
||||
|
||||
func TestAppendBayDevCandidatesWithoutBayLinkDoesNothing(t *testing.T) {
|
||||
measurementSet := &orm.MeasurementSet{
|
||||
AllMeasTags: []string{"current"},
|
||||
CompTagToMeasTags: map[string][]string{
|
||||
"component": {"current"},
|
||||
},
|
||||
}
|
||||
|
||||
appendBayDevCandidates(measurementSet, nil, []string{"dev_instruct"})
|
||||
|
||||
require.Equal(t, []string{"current"}, measurementSet.AllMeasTags)
|
||||
require.Equal(t, []string{"current"}, measurementSet.CompTagToMeasTags["component"])
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"modelRT/common"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestBuildMeasurementTokenValidationQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
wantArgs []any
|
||||
wantWhere string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "seven-part token",
|
||||
token: "grid.zone.station.nspath.component.bay.measurement",
|
||||
wantArgs: []any{"grid", "zone", "station", "nspath", "component", "measurement"},
|
||||
wantWhere: "WHERE g.tagname = ?",
|
||||
},
|
||||
{
|
||||
name: "four-part token",
|
||||
token: "nspath.component.bay.measurement",
|
||||
wantArgs: []any{"nspath", "component", "measurement"},
|
||||
wantWhere: "WHERE c.nspath = ?",
|
||||
},
|
||||
{
|
||||
name: "two-part token",
|
||||
token: "nspath.measurement",
|
||||
wantArgs: []any{"nspath", "measurement"},
|
||||
wantWhere: "WHERE c.nspath = ?",
|
||||
},
|
||||
{
|
||||
name: "non-bay group",
|
||||
token: "nspath.component.rated.attribute",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty segment",
|
||||
token: "nspath..bay.measurement",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid segment count",
|
||||
token: "grid.zone.station",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query, args, err := buildMeasurementTokenValidationQuery(tt.token)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, common.ErrInvalidMeasurementToken)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, query, "INNER JOIN component AS c ON c.global_uuid = m.component_uuid")
|
||||
assert.Contains(t, query, "INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid")
|
||||
assert.Contains(t, query, tt.wantWhere)
|
||||
assert.NotContains(t, query, "grid_idWHERE")
|
||||
assert.Regexp(t, `grid_id\s+WHERE`, query)
|
||||
assert.NotContains(t, query, "\n")
|
||||
assert.NotContains(t, query, "\t")
|
||||
assert.Equal(t, tt.wantArgs, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryMeasurementByIDDoesNotLockRead(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{SkipDefaultTransaction: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.ExpectQuery(`SELECT \* FROM "measurement" WHERE id = \$1 LIMIT \$2`).
|
||||
WithArgs(int64(10), 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "mode"}).AddRow(int64(10), int16(1)))
|
||||
|
||||
measurement, err := QueryMeasurementByID(context.Background(), db, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(10), measurement.ID)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueryMeasurementByIDForUpdateSelectsOnlyRequiredFields(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{SkipDefaultTransaction: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.ExpectQuery(`SELECT "id","mode","data_source" FROM "measurement" WHERE id = \$1 LIMIT \$2 FOR UPDATE`).
|
||||
WithArgs(int64(10), 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "mode", "data_source"}).
|
||||
AddRow(int64(10), int16(1), `{"type":1}`))
|
||||
|
||||
measurement, err := QueryMeasurementByIDForUpdate(context.Background(), db, 10)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(10), measurement.ID)
|
||||
assert.Equal(t, int16(1), measurement.Mode)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestMeasurementOperationAppendSQLIsSingleLine(t *testing.T) {
|
||||
assert.NotContains(t, measurementOperationAppendSQL, "\n")
|
||||
assert.NotContains(t, measurementOperationAppendSQL, "\r")
|
||||
assert.NotContains(t, measurementOperationAppendSQL, "\t")
|
||||
}
|
||||
|
||||
func TestUpdateMeasurementMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
automatic bool
|
||||
wantMode int16
|
||||
}{
|
||||
{name: "manual", automatic: false, wantMode: 0},
|
||||
{name: "automatic", automatic: true, wantMode: 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{SkipDefaultTransaction: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "measurement" SET "mode"=$1 WHERE id = $2`)).
|
||||
WithArgs(tt.wantMode, int64(10)).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err = UpdateMeasurementMode(context.Background(), db, 10, tt.automatic)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMeasurementToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
count int64
|
||||
queryErr error
|
||||
wantErr error
|
||||
}{
|
||||
{name: "exists", count: 1},
|
||||
{name: "not found", count: 0, wantErr: common.ErrMeasurementTokenNotFound},
|
||||
{name: "ambiguous", count: 2, wantErr: common.ErrAmbiguousMeasurementToken},
|
||||
{name: "query failure", queryErr: errors.New("database unavailable")},
|
||||
}
|
||||
|
||||
const token = "nspath.measurement"
|
||||
query, _, err := buildMeasurementTokenValidationQuery(token)
|
||||
require.NoError(t, err)
|
||||
expectedQuery := query
|
||||
for i := 1; strings.Contains(expectedQuery, "?"); i++ {
|
||||
expectedQuery = strings.Replace(expectedQuery, "?", fmt.Sprintf("$%d", i), 1)
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
expectation := mock.ExpectQuery(regexp.QuoteMeta(expectedQuery)).
|
||||
WithArgs("nspath", "measurement")
|
||||
if tt.queryErr != nil {
|
||||
expectation.WillReturnError(tt.queryErr)
|
||||
} else {
|
||||
expectation.WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.count))
|
||||
}
|
||||
|
||||
err = ValidateMeasurementToken(context.Background(), db, token)
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
} else if tt.queryErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.queryErr.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMeasurementRowsQuerySeparatesLimitClause(t *testing.T) {
|
||||
validationQuery, _, err := buildMeasurementTokenValidationQuery("nspath.measurement")
|
||||
require.NoError(t, err)
|
||||
|
||||
query := buildMeasurementRowsQuery(validationQuery)
|
||||
assert.NotContains(t, query, "?LIMIT")
|
||||
assert.Regexp(t, `m\.tag = \?\s+LIMIT 2$`, query)
|
||||
assert.NotContains(t, query, "\n")
|
||||
assert.NotContains(t, query, "\t")
|
||||
}
|
||||
|
||||
func TestQueryMeasurementByDataObjectToken(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||
mock.ExpectQuery(`(?s)SELECT m\.\*.*WHERE c\.nspath = \$1.*AND m\.tag = \$2.*LIMIT 2`).
|
||||
WithArgs("nspath", "measurement").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id",
|
||||
"tag",
|
||||
"name",
|
||||
"mode",
|
||||
"size",
|
||||
"data_source",
|
||||
"event_plan",
|
||||
"binding",
|
||||
"component_uuid",
|
||||
}).AddRow(
|
||||
int64(10),
|
||||
"measurement",
|
||||
"A phase current",
|
||||
int16(1),
|
||||
10,
|
||||
`{"type":1,"io_address":{"channel":"tm1"}}`,
|
||||
`{"enabled":true}`,
|
||||
`{"ct":{"ratio":1}}`,
|
||||
componentUUID,
|
||||
))
|
||||
mock.ExpectQuery(`(?s)SELECT global_uuid, nspath, tag, grid, zone, station.*WHERE global_uuid = \$1.*LIMIT 1`).
|
||||
WithArgs(componentUUID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"global_uuid",
|
||||
"nspath",
|
||||
"tag",
|
||||
"grid",
|
||||
"zone",
|
||||
"station",
|
||||
}).AddRow(componentUUID, "nspath", "component", "grid", "zone", "station"))
|
||||
|
||||
measurement, component, err := QueryMeasurementByDataObjectToken(context.Background(), db, "nspath.measurement")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(10), measurement.ID)
|
||||
assert.Equal(t, int16(1), measurement.Mode)
|
||||
assert.Equal(t, float64(1), measurement.DataSource["type"])
|
||||
assert.Equal(t, "grid", component.GridName)
|
||||
assert.Equal(t, "component", component.Tag)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"modelRT/common"
|
||||
"modelRT/constants"
|
||||
"modelRT/model"
|
||||
"modelRT/orm"
|
||||
modelsql "modelRT/sql"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var parameterTableNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
// ParameterDataObject contains the resolved metadata needed to query a
|
||||
// parameter attribute after its token has passed hierarchy validation.
|
||||
type ParameterDataObject struct {
|
||||
Component orm.Component
|
||||
Project orm.ProjectManager
|
||||
TableName string
|
||||
AttributeGroup string
|
||||
AttributeName string
|
||||
AttributeType string
|
||||
}
|
||||
|
||||
// QueryParameterByDataObjectToken validates a four-part or seven-part
|
||||
// parameter token. The component group resolves directly to the component
|
||||
// table; other groups resolve through project_manager and a dynamic table.
|
||||
func QueryParameterByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*ParameterDataObject, error) {
|
||||
componentQuery, componentArgs, parts, err := buildParameterComponentQuery(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var components []orm.Component
|
||||
if err := db.WithContext(ctx).Raw(componentQuery, componentArgs...).Scan(&components).Error; err != nil {
|
||||
return nil, fmt.Errorf("query component for parameter token %q: %w", token, err)
|
||||
}
|
||||
|
||||
switch len(components) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("%w: component hierarchy for %q", common.ErrParameterTokenNotFound, token)
|
||||
case 1:
|
||||
// Continue by resolving the component model and attribute group.
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: component hierarchy for %q matched more than one record", common.ErrAmbiguousParameterToken, token)
|
||||
}
|
||||
|
||||
attributeGroup := parts[len(parts)-2]
|
||||
attributeName := parts[len(parts)-1]
|
||||
component := components[0]
|
||||
if attributeGroup == "component" {
|
||||
attributeType, err := queryParameterAttributeType(ctx, db, "component", attributeName, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ParameterDataObject{
|
||||
Component: component,
|
||||
TableName: "component",
|
||||
AttributeGroup: attributeGroup,
|
||||
AttributeName: attributeName,
|
||||
AttributeType: attributeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var projects []orm.ProjectManager
|
||||
if err := db.WithContext(ctx).
|
||||
Where("tag = ? AND group_name = ?", component.ModelName, attributeGroup).
|
||||
Limit(2).
|
||||
Find(&projects).Error; err != nil {
|
||||
return nil, fmt.Errorf("query project mapping for parameter token %q: %w", token, err)
|
||||
}
|
||||
|
||||
switch len(projects) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("%w: model %q does not define attribute group %q", common.ErrParameterTokenNotFound, component.ModelName, attributeGroup)
|
||||
case 1:
|
||||
// Continue by validating the dynamic table and attribute.
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: model %q and attribute group %q matched more than one project", common.ErrAmbiguousParameterToken, component.ModelName, attributeGroup)
|
||||
}
|
||||
|
||||
project := projects[0]
|
||||
if !validParameterTableName(project.Name) {
|
||||
return nil, fmt.Errorf("project mapping for parameter token %q contains invalid table name %q", token, project.Name)
|
||||
}
|
||||
attributeType, err := queryParameterAttributeType(ctx, db, project.Name, attributeName, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var recordCount int64
|
||||
if err := db.WithContext(ctx).
|
||||
Table(project.Name).
|
||||
Where("global_uuid = ? AND attribute_group = ?", component.GlobalUUID, attributeGroup).
|
||||
Count(&recordCount).Error; err != nil {
|
||||
return nil, fmt.Errorf("query dynamic record for parameter token %q: %w", token, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case recordCount == 0:
|
||||
return nil, fmt.Errorf("%w: component %q has no %q parameter record", common.ErrParameterTokenNotFound, component.Tag, attributeGroup)
|
||||
case recordCount > 1:
|
||||
return nil, fmt.Errorf("%w: component %q has %d %q parameter records", common.ErrAmbiguousParameterToken, component.Tag, recordCount, attributeGroup)
|
||||
}
|
||||
|
||||
return &ParameterDataObject{
|
||||
Component: component,
|
||||
Project: project,
|
||||
TableName: project.Name,
|
||||
AttributeGroup: attributeGroup,
|
||||
AttributeName: attributeName,
|
||||
AttributeType: attributeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryParameterDataObjectValue returns token7 from the component row or from
|
||||
// a dynamic parameter row identified during token validation.
|
||||
func QueryParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter *ParameterDataObject) (any, error) {
|
||||
if parameter == nil {
|
||||
return nil, fmt.Errorf("parameter data object is nil")
|
||||
}
|
||||
|
||||
var record map[string]any
|
||||
query := db.WithContext(ctx).Table(parameter.TableName)
|
||||
if parameter.AttributeGroup == "component" {
|
||||
query = query.Where("tag = ?", parameter.Component.Tag)
|
||||
} else {
|
||||
query = query.Where("global_uuid = ? AND attribute_group = ?", parameter.Component.GlobalUUID, parameter.AttributeGroup)
|
||||
}
|
||||
result := query.Take(&record)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("query parameter value from table %q: %w", parameter.TableName, result.Error)
|
||||
}
|
||||
|
||||
value, ok := record[parameter.AttributeName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("parameter column %q is missing from table %q result", parameter.AttributeName, parameter.TableName)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// UpdateParameterDataObjectValue writes token7 to the dynamic parameter row
|
||||
// resolved from a data-object token. Component-table attributes are not
|
||||
// supported by the data-object update API.
|
||||
func UpdateParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter *ParameterDataObject, value any) error {
|
||||
if parameter == nil {
|
||||
return fmt.Errorf("parameter data object is nil")
|
||||
}
|
||||
if parameter.AttributeGroup == "component" {
|
||||
return fmt.Errorf("component data-object updates are not supported")
|
||||
}
|
||||
if !validParameterTableName(parameter.TableName) {
|
||||
return fmt.Errorf("invalid parameter table name %q", parameter.TableName)
|
||||
}
|
||||
|
||||
result := db.WithContext(ctx).
|
||||
Table(parameter.TableName).
|
||||
Where("global_uuid = ? AND attribute_group = ?", parameter.Component.GlobalUUID, parameter.AttributeGroup).
|
||||
Update(parameter.AttributeName, value)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("update parameter %q in table %q: %w", parameter.AttributeName, parameter.TableName, result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("update parameter %q in table %q affected no rows", parameter.AttributeName, parameter.TableName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validParameterTableName(tableName string) bool {
|
||||
return parameterTableNamePattern.MatchString(tableName) && constants.IsSupportedParameterTableName(tableName)
|
||||
}
|
||||
|
||||
// QueryParameterAttributeDescription returns the display name registered for
|
||||
// token7 in basic.attribute.
|
||||
func QueryParameterAttributeDescription(ctx context.Context, db *gorm.DB, attributeName string) (string, error) {
|
||||
var rows []struct {
|
||||
Description string `gorm:"column:attribute_name"`
|
||||
}
|
||||
if err := db.WithContext(ctx).
|
||||
Raw(modelsql.ParameterAttributeDescription, attributeName).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return "", fmt.Errorf("query parameter description for attribute %q: %w", attributeName, err)
|
||||
}
|
||||
|
||||
switch len(rows) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("parameter description not found for attribute %q", attributeName)
|
||||
case 1:
|
||||
return rows[0].Description, nil
|
||||
default:
|
||||
return "", fmt.Errorf("ambiguous parameter description for attribute %q", attributeName)
|
||||
}
|
||||
}
|
||||
|
||||
func queryParameterAttributeType(ctx context.Context, db *gorm.DB, tableName, attributeName, token string) (string, error) {
|
||||
var attributeType string
|
||||
result := db.WithContext(ctx).
|
||||
Raw(modelsql.ParameterAttributeColumnType, tableName, attributeName).
|
||||
Scan(&attributeType)
|
||||
if result.Error != nil {
|
||||
return "", fmt.Errorf("validate attribute column for parameter token %q: %w", token, result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 || attributeType == "" {
|
||||
return "", fmt.Errorf("%w: column %q does not exist in parameter table %q", common.ErrParameterTokenNotFound, attributeName, tableName)
|
||||
}
|
||||
return strings.ToUpper(attributeType), nil
|
||||
}
|
||||
|
||||
func buildParameterComponentQuery(token string) (string, []any, []string, error) {
|
||||
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("%w %q: %v", common.ErrInvalidParameterToken, token, err)
|
||||
}
|
||||
if dataObjectType != constants.DataObjectTypeParameter {
|
||||
return "", nil, nil, fmt.Errorf("%w %q: token does not identify a parameter", common.ErrInvalidParameterToken, token)
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
var where string
|
||||
var args []any
|
||||
switch len(parts) {
|
||||
case 7:
|
||||
where = modelsql.ParameterSevenPartTokenWhere
|
||||
args = []any{parts[0], parts[1], parts[2], parts[3], parts[4]}
|
||||
case 4:
|
||||
where = modelsql.ParameterFourPartTokenWhere
|
||||
args = []any{parts[0], parts[1]}
|
||||
default:
|
||||
return "", nil, nil, fmt.Errorf("%w %q: expected 4 or 7 segments", common.ErrInvalidParameterToken, token)
|
||||
}
|
||||
|
||||
query := compactParameterSQL(strings.Join([]string{
|
||||
modelsql.ParameterComponentQueryBase,
|
||||
where,
|
||||
modelsql.ParameterLimitTwo,
|
||||
}, "\n"))
|
||||
return query, args, parts, nil
|
||||
}
|
||||
|
||||
func compactParameterSQL(statement string) string {
|
||||
return strings.Join(strings.Fields(statement), " ")
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"modelRT/common"
|
||||
"modelRT/orm"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestBuildParameterComponentQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
wantArgs []any
|
||||
wantWhere string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "seven-part parameter",
|
||||
token: "grid.zone.station.nspath.component.stable.attribute",
|
||||
wantArgs: []any{"grid", "zone", "station", "nspath", "component"},
|
||||
wantWhere: "WHERE g.tagname = ?",
|
||||
},
|
||||
{
|
||||
name: "four-part local parameter",
|
||||
token: "nspath.component.rated.attribute",
|
||||
wantArgs: []any{"nspath", "component"},
|
||||
wantWhere: "s.is_local = TRUE",
|
||||
},
|
||||
{
|
||||
name: "measurement token",
|
||||
token: "nspath.component.bay.measurement",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty segment",
|
||||
token: "nspath..stable.attribute",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query, args, _, err := buildParameterComponentQuery(tt.token)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, common.ErrInvalidParameterToken)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, query, "INNER JOIN station AS s ON s.id = c.station_id")
|
||||
assert.Contains(t, query, tt.wantWhere)
|
||||
assert.Equal(t, tt.wantArgs, args)
|
||||
assert.NotContains(t, query, "\n")
|
||||
assert.NotContains(t, query, "\t")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryParameterByDataObjectToken(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
token = "grid.zone.station.nspath.component.stable.rated_voltage"
|
||||
componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||
)
|
||||
|
||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE g\.tagname = \$1.*AND c\.tag = \$5.*LIMIT 2`).
|
||||
WithArgs("grid", "zone", "station", "nspath", "component").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"global_uuid", "nspath", "tag", "model_name", "station_id",
|
||||
}).AddRow(componentUUID, "nspath", "component", "bus_1", int64(10)))
|
||||
|
||||
mock.ExpectQuery(`SELECT \* FROM "project_manager" WHERE tag = \$1 AND group_name = \$2 LIMIT \$3`).
|
||||
WithArgs("bus_1", "stable", 2).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id", "name", "tag", "meta_model", "group_name", "link_type", "check_state", "ispublic",
|
||||
}).AddRow(
|
||||
int32(1),
|
||||
"bus_bus_1_stable",
|
||||
"bus_1",
|
||||
"bus",
|
||||
"stable",
|
||||
int32(0),
|
||||
`{"checkState":[{"name":"rated_voltage","checked":1}]}`,
|
||||
false,
|
||||
))
|
||||
|
||||
mock.ExpectQuery(`(?s)SELECT pg_catalog\.format_type.*pg_catalog\.pg_attribute.*c\.relname = \$1.*a\.attname = \$2.*LIMIT 1`).
|
||||
WithArgs("bus_bus_1_stable", "rated_voltage").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"format_type"}).AddRow("double precision"))
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "bus_bus_1_stable" WHERE global_uuid = $1 AND attribute_group = $2`)).
|
||||
WithArgs(componentUUID, "stable").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(int64(1)))
|
||||
|
||||
parameter, err := QueryParameterByDataObjectToken(context.Background(), db, token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "component", parameter.Component.Tag)
|
||||
assert.Equal(t, "bus_bus_1_stable", parameter.Project.Name)
|
||||
assert.Equal(t, "bus_bus_1_stable", parameter.TableName)
|
||||
assert.Equal(t, "stable", parameter.AttributeGroup)
|
||||
assert.Equal(t, "rated_voltage", parameter.AttributeName)
|
||||
assert.Equal(t, "DOUBLE PRECISION", parameter.AttributeType)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueryParameterDataObjectValue(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||
parsedUUID, err := uuid.FromString(componentUUID)
|
||||
require.NoError(t, err)
|
||||
parameter := &ParameterDataObject{
|
||||
Component: orm.Component{GlobalUUID: parsedUUID},
|
||||
Project: orm.ProjectManager{
|
||||
Name: "bus_bus_1_stable",
|
||||
},
|
||||
TableName: "bus_bus_1_stable",
|
||||
AttributeGroup: "stable",
|
||||
AttributeName: "rated_voltage",
|
||||
}
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bus_bus_1_stable" WHERE global_uuid = $1 AND attribute_group = $2 LIMIT $3`)).
|
||||
WithArgs(componentUUID, "stable", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"global_uuid", "attribute_group", "rated_voltage",
|
||||
}).AddRow(componentUUID, "stable", float64(220)))
|
||||
|
||||
value, err := QueryParameterDataObjectValue(context.Background(), db, parameter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, float64(220), value)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestUpdateParameterDataObjectValue(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{SkipDefaultTransaction: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||
parsedUUID, err := uuid.FromString(componentUUID)
|
||||
require.NoError(t, err)
|
||||
parameter := &ParameterDataObject{
|
||||
Component: orm.Component{GlobalUUID: parsedUUID},
|
||||
TableName: "bus_bus_1_rated",
|
||||
AttributeGroup: "rated",
|
||||
AttributeName: "unom_kv",
|
||||
}
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "bus_bus_1_rated" SET "unom_kv"=$1 WHERE global_uuid = $2 AND attribute_group = $3`)).
|
||||
WithArgs("15.2", componentUUID, "rated").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
err = UpdateParameterDataObjectValue(context.Background(), db, parameter, "15.2")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestUpdateParameterDataObjectValueRejectsComponent(t *testing.T) {
|
||||
err := UpdateParameterDataObjectValue(context.Background(), &gorm.DB{}, &ParameterDataObject{
|
||||
TableName: "component",
|
||||
AttributeGroup: "component",
|
||||
AttributeName: "global_uuid",
|
||||
}, "uuid")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not supported")
|
||||
}
|
||||
|
||||
func TestQueryComponentParameterByDataObjectToken(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
token = "grid.zone.station.nspath.component.component.description"
|
||||
componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||
)
|
||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE g\.tagname = \$1.*AND c\.tag = \$5.*LIMIT 2`).
|
||||
WithArgs("grid", "zone", "station", "nspath", "component").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"global_uuid", "nspath", "tag", "model_name", "grid", "zone", "station", "station_id",
|
||||
}).AddRow(componentUUID, "nspath", "component", "bus_1", "grid", "zone", "station", int64(10)))
|
||||
mock.ExpectQuery(`(?s)SELECT pg_catalog\.format_type.*c\.relname = \$1.*a\.attname = \$2.*LIMIT 1`).
|
||||
WithArgs("component", "description").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"format_type"}).AddRow("character varying(512)"))
|
||||
|
||||
parameter, err := QueryParameterByDataObjectToken(context.Background(), db, token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "component", parameter.TableName)
|
||||
assert.Equal(t, "component", parameter.AttributeGroup)
|
||||
assert.Equal(t, "description", parameter.AttributeName)
|
||||
assert.Equal(t, "CHARACTER VARYING(512)", parameter.AttributeType)
|
||||
assert.Empty(t, parameter.Project.Name)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueryComponentParameterValue(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
parameter := &ParameterDataObject{
|
||||
Component: orm.Component{Tag: "component"},
|
||||
TableName: "component",
|
||||
AttributeGroup: "component",
|
||||
AttributeName: "description",
|
||||
}
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "component" WHERE tag = $1 LIMIT $2`)).
|
||||
WithArgs("component", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"tag", "description"}).AddRow("component", "测试组件"))
|
||||
|
||||
value, err := QueryParameterDataObjectValue(context.Background(), db, parameter)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "测试组件", value)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueryParameterAttributeDescription(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.ExpectQuery(`(?s)SELECT attribute_name.*FROM basic\.attribute.*WHERE attribute = \$1.*LIMIT 2`).
|
||||
WithArgs("rated_voltage").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"attribute_name"}).AddRow("额定电压"))
|
||||
|
||||
description, err := QueryParameterAttributeDescription(context.Background(), db, "rated_voltage")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "额定电压", description)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueryParameterByDataObjectTokenComponentNotFound(t *testing.T) {
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE c\.nspath = \$1.*AND c\.tag = \$2.*s\.is_local = TRUE.*LIMIT 2`).
|
||||
WithArgs("nspath", "component").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"global_uuid"}))
|
||||
|
||||
_, err = QueryParameterByDataObjectToken(
|
||||
context.Background(),
|
||||
db,
|
||||
"nspath.component.stable.rated_voltage",
|
||||
)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, common.ErrParameterTokenNotFound)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// Package database define database operation functions
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"modelRT/orm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// QueryMeasurementByID return the result of query circuit diagram component measurement info by id from postgresDB
|
||||
func QueryMeasurementByID(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
|
||||
var measurement orm.Measurement
|
||||
// ctx超时判断
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where("id = ?", id).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
First(&measurement)
|
||||
|
||||
if result.Error != nil {
|
||||
return orm.Measurement{}, result.Error
|
||||
}
|
||||
return measurement, nil
|
||||
}
|
||||
|
||||
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
||||
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
||||
// TODO parse token to avoid SQL injection
|
||||
|
||||
var component orm.Measurement
|
||||
// ctx超时判断
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Where(" = ?", token).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
First(&component)
|
||||
|
||||
if result.Error != nil {
|
||||
return orm.Measurement{}, result.Error
|
||||
}
|
||||
return component, nil
|
||||
}
|
||||
|
||||
// GetAllMeasurements define func to query all measurement info from postgresDB
|
||||
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
||||
var measurements []orm.Measurement
|
||||
|
||||
// ctx超时判断
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return measurements, nil
|
||||
}
|
||||
|
|
@ -3,8 +3,11 @@ package database
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"modelRT/constants"
|
||||
"modelRT/diagram"
|
||||
"modelRT/logger"
|
||||
"modelRT/orm"
|
||||
"modelRT/sql"
|
||||
|
|
@ -21,32 +24,124 @@ func QueryTopologic(ctx context.Context, tx *gorm.DB) ([]orm.Topologic, error) {
|
|||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Find(&topologics)
|
||||
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Raw(sql.RecursiveSQL, constants.UUIDNilStr).Scan(&topologics)
|
||||
if result.Error != nil {
|
||||
logger.Error(ctx, "query circuit diagram topologic info failed", "error", result.Error)
|
||||
logger.Error(ctx, "query circuit diagram topologic info by start node uuid failed", "start_node_uuid", constants.UUIDNilStr, "error", result.Error)
|
||||
return nil, result.Error
|
||||
}
|
||||
return topologics, nil
|
||||
}
|
||||
|
||||
// QueryTopologicByStartUUID returns all directed edges reachable from startUUID.
|
||||
// It is used by point-to-point topology reachability checks and intentionally
|
||||
// does not depend on the legacy all-zero UUID virtual root.
|
||||
func QueryTopologicByStartUUID(ctx context.Context, tx *gorm.DB, startUUID uuid.UUID) ([]orm.Topologic, error) {
|
||||
var topologics []orm.Topologic
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := tx.WithContext(cancelCtx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Raw(sql.RecursiveTopologicByStartSQL, startUUID).
|
||||
Scan(&topologics)
|
||||
if result.Error != nil {
|
||||
logger.Error(ctx, "query topologic by start uuid failed", "start_uuid", startUUID, "error", result.Error)
|
||||
return nil, result.Error
|
||||
// QueryTopologicFromDB return the result of query topologic info from DB
|
||||
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB) (*diagram.MultiBranchTreeNode, error) {
|
||||
topologicInfos, err := QueryTopologic(ctx, tx)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "query topologic info failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return topologics, nil
|
||||
|
||||
tree, err := BuildMultiBranchTree(topologicInfos)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "init topologic failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// InitCircuitDiagramTopologic return circuit diagram topologic info from postgres
|
||||
func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic) error {
|
||||
var rootVertex *diagram.MultiBranchTreeNode
|
||||
for _, node := range topologicNodes {
|
||||
if node.UUIDFrom == constants.UUIDNil {
|
||||
rootVertex = diagram.NewMultiBranchTree(node.UUIDFrom)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rootVertex == nil {
|
||||
return fmt.Errorf("root vertex is nil")
|
||||
}
|
||||
|
||||
for _, node := range topologicNodes {
|
||||
if node.UUIDFrom == constants.UUIDNil {
|
||||
nodeVertex := diagram.NewMultiBranchTree(node.UUIDTo)
|
||||
rootVertex.AddChild(nodeVertex)
|
||||
}
|
||||
}
|
||||
|
||||
node := rootVertex
|
||||
for _, nodeVertex := range node.Children {
|
||||
nextVertexs := make([]*diagram.MultiBranchTreeNode, 0)
|
||||
nextVertexs = append(nextVertexs, nodeVertex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO 电流互感器不单独划分间隔,以母线、浇筑母线、变压器为间隔原件
|
||||
func IntervalBoundaryDetermine(uuid uuid.UUID) bool {
|
||||
diagram.GetComponentMap(uuid.String())
|
||||
// TODO 判断 component 的类型是否为间隔
|
||||
// TODO 0xA1B2C3D4,高四位表示可以成为间隔的compoent类型的值为FFFF,普通 component 类型的值为 0000。低四位中前二位表示component的一级类型,例如母线 PT、母联/母分、进线等,低四位中后二位表示一级类型中包含的具体类型,例如母线 PT中包含的电压互感器、隔离开关、接地开关、避雷器、带电显示器等。
|
||||
num := uint32(0xA1B2C3D4) // 八位16进制数
|
||||
high16 := uint16(num >> 16)
|
||||
fmt.Printf("原始值: 0x%X\n", num) // 输出: 0xA1B2C3D4
|
||||
fmt.Printf("高十六位: 0x%X\n", high16) // 输出: 0xA1B2
|
||||
return true
|
||||
}
|
||||
|
||||
// BuildMultiBranchTree return the multi branch tree by topologic info and component type map
|
||||
func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeNode, error) {
|
||||
nodeMap := make(map[uuid.UUID]*diagram.MultiBranchTreeNode, len(topologics)*2)
|
||||
|
||||
for _, topo := range topologics {
|
||||
if _, exists := nodeMap[topo.UUIDFrom]; !exists {
|
||||
// skip special uuid
|
||||
if topo.UUIDTo != constants.UUIDNil {
|
||||
nodeMap[topo.UUIDFrom] = &diagram.MultiBranchTreeNode{
|
||||
ID: topo.UUIDFrom,
|
||||
Children: make([]*diagram.MultiBranchTreeNode, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := nodeMap[topo.UUIDTo]; !exists {
|
||||
// skip special uuid
|
||||
if topo.UUIDTo != constants.UUIDNil {
|
||||
nodeMap[topo.UUIDTo] = &diagram.MultiBranchTreeNode{
|
||||
ID: topo.UUIDTo,
|
||||
Children: make([]*diagram.MultiBranchTreeNode, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, topo := range topologics {
|
||||
var parent *diagram.MultiBranchTreeNode
|
||||
if topo.UUIDFrom == constants.UUIDNil {
|
||||
parent = &diagram.MultiBranchTreeNode{
|
||||
ID: constants.UUIDNil,
|
||||
}
|
||||
nodeMap[constants.UUIDNil] = parent
|
||||
} else {
|
||||
parent = nodeMap[topo.UUIDFrom]
|
||||
}
|
||||
|
||||
var child *diagram.MultiBranchTreeNode
|
||||
if topo.UUIDTo == constants.UUIDNil {
|
||||
child = &diagram.MultiBranchTreeNode{
|
||||
ID: topo.UUIDTo,
|
||||
}
|
||||
} else {
|
||||
child = nodeMap[topo.UUIDTo]
|
||||
}
|
||||
child.Parent = parent
|
||||
parent.Children = append(parent.Children, child)
|
||||
}
|
||||
|
||||
// return root vertex
|
||||
root, exists := nodeMap[constants.UUIDNil]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("root node not found")
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func UpdateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
|
|||
Name: componentInfo.Name,
|
||||
Context: componentInfo.Context,
|
||||
Op: componentInfo.Op,
|
||||
TS: time.Now(),
|
||||
Ts: time.Now(),
|
||||
}
|
||||
|
||||
result = tx.Model(&orm.Component{}).WithContext(cancelCtx).Where("GLOBAL_UUID = ?", component.GlobalUUID).Updates(&updateParams)
|
||||
|
|
|
|||
884
deploy/deploy.md
884
deploy/deploy.md
|
|
@ -1,12 +1,12 @@
|
|||
# 项目服务部署指南
|
||||
# 项目依赖服务部署指南
|
||||
|
||||
本项目依赖于 `PostgreSQL` 数据库和 `Redis Stack Server`(包含 `Redisearch` 等模块)部署文档将使用 `Docker` 容器化技术部署这两个依赖服务
|
||||
本项目依赖于 $\text{PostgreSQL}$ 数据库和 $\text{Redis Stack Server}$(包含 $\text{Redisearch}$ 等模块)部署文档将使用 $\text{Docker}$ 容器化技术部署这两个依赖服务
|
||||
|
||||
## 前提条件
|
||||
|
||||
1. 已安装 `Docker`
|
||||
1. 已安装 $\text{Docker}$
|
||||
2. 下载相关容器镜像
|
||||
3. 确保主机的 `5432` 端口(`Postgres`)和 `6379` 端口(`Redis`)未被占用
|
||||
3. 确保主机的 $\text{5432}$ 端口($\text{Postgres}$)和 $\text{6379}$ 端口($\text{Redis}$)未被占用
|
||||
|
||||
### 1\. 部署 PostgreSQL 数据库
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
#### 1.1 部署命令
|
||||
|
||||
运行以下命令启动 `PostgreSQL` 容器
|
||||
运行以下命令启动 $\text{PostgreSQL}$ 容器
|
||||
|
||||
```bash
|
||||
docker run --name postgres \
|
||||
|
|
@ -45,75 +45,13 @@ docker ps -a |grep postgres
|
|||
docker logs postgres
|
||||
```
|
||||
|
||||
#### 1.4 初始化异步任务表
|
||||
|
||||
`PostgreSQL` 启动后执行以下建表语句,创建异步任务系统所需的两张表:
|
||||
|
||||
```sql
|
||||
-- ==========================================
|
||||
-- 表: async_task
|
||||
-- 说明: 存储异步任务的生命周期跟踪信息
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS async_task (
|
||||
task_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
params JSONB,
|
||||
created_at BIGINT NOT NULL,
|
||||
finished_at BIGINT,
|
||||
started_at BIGINT,
|
||||
execution_time BIGINT,
|
||||
progress INTEGER,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
max_retry_count INTEGER DEFAULT 3,
|
||||
next_retry_time BIGINT,
|
||||
retry_delay INTEGER DEFAULT 5000,
|
||||
priority INTEGER DEFAULT 5,
|
||||
queue_name VARCHAR(100) DEFAULT 'default',
|
||||
worker_id VARCHAR(50),
|
||||
failure_reason TEXT,
|
||||
stack_trace TEXT,
|
||||
created_by VARCHAR(100)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_task_type ON async_task(task_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_status ON async_task(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_created_at ON async_task(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_finished_at ON async_task(finished_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_started_at ON async_task(started_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_next_retry_time ON async_task(next_retry_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_priority ON async_task(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_async_task_status_retry ON async_task(status, next_retry_time)
|
||||
WHERE status = 'FAILED' AND next_retry_time IS NOT NULL;
|
||||
|
||||
-- ==========================================
|
||||
-- 表: async_task_result
|
||||
-- 说明: 存储异步任务的执行结果
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS async_task_result (
|
||||
task_id UUID PRIMARY KEY,
|
||||
result JSONB,
|
||||
error_code INTEGER,
|
||||
error_message TEXT,
|
||||
error_detail JSONB,
|
||||
execution_time BIGINT NOT NULL DEFAULT 0,
|
||||
memory_usage BIGINT,
|
||||
cpu_usage DOUBLE PRECISION,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
completed_at BIGINT NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE async_task IS '异步任务生命周期跟踪表';
|
||||
COMMENT ON TABLE async_task_result IS '异步任务执行结果表';
|
||||
```
|
||||
|
||||
### 2\. 部署 Redis Stack Server
|
||||
|
||||
我们将使用 `redis/redis-stack-server:latest` 镜像该镜像内置了 `Redisearch` 模块,用于 `ModelRT` 项目中补全功能
|
||||
我们将使用 `redis/redis-stack-server:latest` 镜像该镜像内置了 $\text{Redisearch}$ 模块,用于 $\text{ModelRT}$ 项目中补全功能
|
||||
|
||||
#### 2.1 部署命令
|
||||
|
||||
运行以下命令启动 `Redis Stack Server` 容器
|
||||
运行以下命令启动 $\text{Redis Stack Server}$ 容器
|
||||
|
||||
```bash
|
||||
docker run --name redis -p 6379:6379 \
|
||||
|
|
@ -130,7 +68,7 @@ docker run --name redis -p 6379:6379 \
|
|||
| **地址** | `localhost:6379` | |
|
||||
| **密码** | **无** | 默认未设置密码 |
|
||||
|
||||
> **注意:** 生产环境中建议使用 `-e REDIS_PASSWORD=<your_secure_password>` 参数来设置 `Redis` 访问密码
|
||||
> **注意:** 生产环境中建议使用 `-e REDIS_PASSWORD=<your_secure_password>` 参数来设置 $\text{Redis}$ 访问密码
|
||||
|
||||
#### 2.3 状态检查
|
||||
|
||||
|
|
@ -198,7 +136,7 @@ VALUES
|
|||
'ns1', 'tag1', 'component1', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
|
|
@ -211,7 +149,7 @@ VALUES
|
|||
'ns2', 'tag2', 'component2', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
|
|
@ -224,72 +162,7 @@ VALUES
|
|||
'ns3', 'tag3', 'component3', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station2', 2,
|
||||
-1,
|
||||
true,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
'{}',
|
||||
-1,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'70c190f2-8a60-42a9-b143-ec5f87e0aa6b',
|
||||
'ns4', 'tag4', 'component4', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
'{}',
|
||||
-1,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'10f155cf-bd27-4557-85b2-d126b6e2657f',
|
||||
'ns5', 'tag5', 'component5', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
'{}',
|
||||
-1,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'e32bc0be-67f4-4d79-a5da-eaa40a5bd77d',
|
||||
'ns6', 'tag6', 'component6', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
'{}',
|
||||
-1,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'70c190f2-8a75-42a9-b166-ec5f87e0aa6b',
|
||||
'ns7', 'tag7', 'component7', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
'{}',
|
||||
-1,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'70c200f2-8a75-42a9-c166-bf5f87e0aa6b',
|
||||
'ns8', 'tag8', 'component8', 'bus_1', '',
|
||||
'grid1', 'zone1', 'station1', 1,
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
-1, -1,
|
||||
'{}',
|
||||
'{}',
|
||||
|
|
@ -403,46 +276,46 @@ go run deploy/redis-test-data/measurments-recommend/measurement_injection.go
|
|||
|
||||
| 类别 | 参数名 | 作用描述 | 示例值 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Postgres** | `host` | PostgreSQL 数据库服务器的 `IP` 地址或域名。 | `"192.168.1.101"` |
|
||||
| **Postgres** | `host` | PostgreSQL 数据库服务器的 $\text{IP}$ 地址或域名。 | `"192.168.1.101"` |
|
||||
| | `port` | PostgreSQL 数据库服务器的端口号。 | `5432` |
|
||||
| | `database` | 连接的数据库名称。 | `"demo"` |
|
||||
| | `user` | 连接数据库所使用的用户名。 | `"postgres"` |
|
||||
| | `password` | 连接数据库所使用的密码。 | `"coslight"` |
|
||||
| **Kafka** | `servers` | Kafka 集群的 `Bootstrap Server` 地址列表(通常是 `host:port` 形式,多个地址用逗号分隔)。 | `"localhost:9092"` |
|
||||
| **Kafka** | `servers` | Kafka 集群的 $\text{Bootstrap Server}$ 地址列表(通常是 $\text{host:port}$ 形式,多个地址用逗号分隔)。 | `"localhost:9092"` |
|
||||
| | `port` | Kafka 服务器的端口号。 | `9092` |
|
||||
| | `group_id` | 消费者组 `ID`,用于标识和管理一组相关的消费者。 | `"modelRT"` |
|
||||
| | `group_id` | 消费者组 $\text{ID}$,用于标识和管理一组相关的消费者。 | `"modelRT"` |
|
||||
| | `topic` | Kafka 消息的主题名称。 | `""` |
|
||||
| | `auto_offset_reset` | 消费者首次启动或 `Offset` 无效时,从哪个位置开始消费(如 `earliest` 或 `latest`)。 | `"earliest"` |
|
||||
| | `enable_auto_commit` | 是否自动提交 `Offset`。设为 `false` 通常用于手动控制 `Offset` 提交。 | `"false"` |
|
||||
| | `auto_offset_reset` | 消费者首次启动或 $\text{Offset}$ 无效时,从哪个位置开始消费(如 `earliest` 或 `latest`)。 | `"earliest"` |
|
||||
| | `enable_auto_commit` | 是否自动提交 $\text{Offset}$。设为 $\text{false}$ 通常用于手动控制 $\text{Offset}$ 提交。 | `"false"` |
|
||||
| | `read_message_time_duration` | 读取消息时的超时或等待时间。 | `”0.5s"` |
|
||||
| **Logger (Zap)** | `mode` | 日志模式,通常为 `development`(开发)或 `production`(生产)。影响日志格式。 | `"development"` |
|
||||
| | `level` | 最低日志级别(如 `debug`, `info`, `warn`, `error`)。 | `"debug"` |
|
||||
| | `level` | 最低日志级别(如 $\text{debug, info, warn, error}$)。 | `"debug"` |
|
||||
| | `filepath` | 日志文件的输出路径和名称格式(`%s` 会被替换为日期等)。 | `"/Users/douxu/Workspace/coslight/modelRT/modelRT-%s.log"` |
|
||||
| | `maxsize` | 单个日志文件最大大小(单位:`MB`)。 | `1` |
|
||||
| | `maxsize` | 单个日志文件最大大小(单位:$\text{MB}$)。 | `1` |
|
||||
| | `maxbackups` | 保留旧日志文件的最大个数。 | `5` |
|
||||
| | `maxage` | 保留旧日志文件的最大天数。 | `30` |
|
||||
| | `compress` | 是否压缩备份的日志文件。 | `false` |
|
||||
| **Ants Pool** | `parse_concurrent_quantity` | 用于解析任务的协程池最大并发数量。 | `10` |
|
||||
| | `rtd_receive_concurrent_quantity` | 用于实时数据接收任务的协程池最大并发数量。 | `10` |
|
||||
| **Locker Redis** | `addr` | 分布式锁服务所使用的 `Redis` 地址。 | `"127.0.0.1:6379"` |
|
||||
| | `password` | `Locker Redis` 的密码。 | `""` |
|
||||
| | `db` | `Locker Redis` 使用的数据库编号。 | `1` |
|
||||
| | `poolsize` | `Locker Redis` 连接池的最大连接数。 | `50` |
|
||||
| | `timeout` | `Locker Redis` 连接操作的超时时间(单位:毫秒)。 | `10` |
|
||||
| **Storage Redis** | `addr` | 数据存储服务所使用的 `Redis` 地址(例如 `Redisearch`)。 | `"127.0.0.1:6379"` |
|
||||
| | `password` | `Storage Redis` 的密码。 | `""` |
|
||||
| | `db` | `Storage Redis` 使用的数据库编号。 | `0` |
|
||||
| | `poolsize` | `Storage Redis` 连接池的最大连接数。 | `50` |
|
||||
| | `timeout` | `Storage Redis` 连接操作的超时时间(单位:毫秒)。 | `10` |
|
||||
| **Base Config** | `grid_id` | 项目所操作的默认电网 `ID`。 | `1` |
|
||||
| | `zone_id` | 项目所操作的默认区域 `ID`。 | `1` |
|
||||
| | `station_id` | 项目所操作的默认变电站 `ID`。 | `1` |
|
||||
| **Locker Redis** | `addr` | 分布式锁服务所使用的 $\text{Redis}$ 地址。 | `"127.0.0.1:6379"` |
|
||||
| | `password` | $\text{Locker Redis}$ 的密码。 | `""` |
|
||||
| | `db` | $\text{Locker Redis}$ 使用的数据库编号。 | `1` |
|
||||
| | `poolsize` | $\text{Locker Redis}$ 连接池的最大连接数。 | `50` |
|
||||
| | `timeout` | $\text{Locker Redis}$ 连接操作的超时时间(单位:毫秒)。 | `10` |
|
||||
| **Storage Redis** | `addr` | 数据存储服务所使用的 $\text{Redis}$ 地址(例如 $\text{Redisearch}$)。 | `"127.0.0.1:6379"` |
|
||||
| | `password` | $\text{Storage Redis}$ 的密码。 | `""` |
|
||||
| | `db` | $\text{Storage Redis}$ 使用的数据库编号。 | `0` |
|
||||
| | `poolsize` | $\text{Storage Redis}$ 连接池的最大连接数。 | `50` |
|
||||
| | `timeout` | $\text{Storage Redis}$ 连接操作的超时时间(单位:毫秒)。 | `10` |
|
||||
| **Base Config** | `grid_id` | 项目所操作的默认电网 $\text{ID}$。 | `1` |
|
||||
| | `zone_id` | 项目所操作的默认区域 $\text{ID}$。 | `1` |
|
||||
| | `station_id` | 项目所操作的默认变电站 $\text{ID}$。 | `1` |
|
||||
| **Service Config** | `service_name` | 服务名称,用于日志、监控等标识。 | `"modelRT"` |
|
||||
| | `secret_key` | 服务内部使用的秘钥,用于签名或认证。 | `"modelrt_key"` |
|
||||
| **DataRT API** | `host` | 外部 `DataRT` 服务的主机地址。 | `"http://127.0.0.1"` |
|
||||
| | `port` | `DataRT` 服务的端口号。 | `8888` |
|
||||
| | `polling_api` | 轮询数据的 `API` 路径。 | `"datart/getPointData"` |
|
||||
| | `polling_api_method` | 调用该 `API` 使用的 `HTTP` 方法。 | `"GET"` |
|
||||
| **DataRT API** | `host` | 外部 $\text{DataRT}$ 服务的主机地址。 | `"http://127.0.0.1"` |
|
||||
| | `port` | $\text{DataRT}$ 服务的端口号。 | `8888` |
|
||||
| | `polling_api` | 轮询数据的 $\text{API}$ 路径。 | `"datart/getPointData"` |
|
||||
| | `polling_api_method` | 调用该 $\text{API}$ 使用的 $\text{HTTP}$ 方法。 | `"GET"` |
|
||||
|
||||
#### 3.2 编译 ModelRT 服务
|
||||
|
||||
|
|
@ -463,691 +336,16 @@ go build -o model-rt main.go
|
|||
在发现控制台输出如下信息`starting ModelRT server`
|
||||
后即代表服务启动成功
|
||||
|
||||
### 4\. 部署基础依赖(Kubernetes)
|
||||
### 4\. 后续操作(停止与清理)
|
||||
|
||||
Redis 和 RabbitMQ 部署在 Minikube 中,YAML 文件位于 `deploy/k8s/`。RabbitMQ 启用双向 TLS(mTLS),客户端以 X.509 证书的 CN 字段作为用户名进行认证。
|
||||
|
||||
#### 4.1 部署 Redis
|
||||
#### 4.1 停止容器
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/redis-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/redis-service.yaml
|
||||
```
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **镜像** | `redis/redis-stack-server:latest` | 内置 Redisearch 模块 |
|
||||
| **NodePort** | `30001` | 集群外访问端口 |
|
||||
|
||||
#### 4.2 RabbitMQ TLS 证书生成
|
||||
|
||||
RabbitMQ 配置为仅允许 TLS 连接(`listeners.tcp = none`),所有客户端须持有由同一 CA 签发的证书。
|
||||
|
||||
##### 4.2.1 生成根 CA
|
||||
|
||||
```bash
|
||||
# 克隆 tls-gen 工具
|
||||
git clone https://github.com/rabbitmq/tls-gen.git
|
||||
cd tls-gen/basic
|
||||
|
||||
# 生成根 CA(结果在 result/ 目录)
|
||||
make CN=rabbitmq-server
|
||||
# ca_certificate.pem 和 ca_key.pem 生成于 result/
|
||||
```
|
||||
|
||||
##### 4.2.2 生成服务器证书
|
||||
|
||||
服务器证书需包含 SAN(Subject Alternative Name),使其同时匹配集群内 DNS 和 Minikube IP。
|
||||
|
||||
创建 `server.cnf`:
|
||||
|
||||
```text
|
||||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = CN
|
||||
ST = Beijing
|
||||
L = Beijing
|
||||
O = coslight
|
||||
CN = rabbitmq-server
|
||||
|
||||
[v3_server]
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth, clientAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = rabbitmq-server
|
||||
DNS.2 = rabbitmq-service.default.svc.cluster.local
|
||||
DNS.3 = localhost
|
||||
IP.1 = 192.168.49.2
|
||||
IP.2 = 127.0.0.1
|
||||
```
|
||||
|
||||
生成证书:
|
||||
|
||||
```bash
|
||||
# 将 ca_certificate.pem 和 ca_key.pem(即 cakey.pem)放在当前目录
|
||||
openssl genrsa -out server_key.pem 2048
|
||||
|
||||
openssl req -new -key server_key.pem -out server_cert.csr -config server.cnf
|
||||
|
||||
openssl x509 -req -in server_cert.csr \
|
||||
-CA ca_certificate.pem -CAkey cakey.pem -CAcreateserial \
|
||||
-out server_certificate.pem -days 730 -sha256 \
|
||||
-extfile server.cnf -extensions v3_server
|
||||
|
||||
rm server_cert.csr
|
||||
```
|
||||
|
||||
##### 4.2.3 生成 ModelRT 客户端证书
|
||||
|
||||
CN 必须与 RabbitMQ 中注册的用户名一致(`modelrt-client`)。
|
||||
|
||||
创建 `modelrt.cnf`:
|
||||
|
||||
```text
|
||||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = CN
|
||||
ST = Beijing
|
||||
L = Beijing
|
||||
O = coslight
|
||||
CN = modelrt-client
|
||||
|
||||
[v3_client]
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
```
|
||||
|
||||
生成证书:
|
||||
|
||||
```bash
|
||||
openssl genrsa -out modelrt_client_key.pem 2048
|
||||
|
||||
openssl req -new -key modelrt_client_key.pem \
|
||||
-out modelrt_client.csr -config modelrt.cnf
|
||||
|
||||
openssl x509 -req -in modelrt_client.csr \
|
||||
-CA ca_certificate.pem -CAkey cakey.pem -CAcreateserial \
|
||||
-out modelrt_client_cert.pem -days 365 \
|
||||
-extensions v3_client -extfile modelrt.cnf
|
||||
|
||||
rm modelrt_client.csr
|
||||
```
|
||||
|
||||
##### 4.2.4 生成 EventRT 客户端证书
|
||||
|
||||
创建 `eventrt.cnf`(CN 改为 `eventrt-client`):
|
||||
|
||||
```text
|
||||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = CN
|
||||
ST = Beijing
|
||||
L = Beijing
|
||||
O = coslight
|
||||
CN = eventrt-client
|
||||
|
||||
[v3_client]
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
```
|
||||
|
||||
生成证书:
|
||||
|
||||
```bash
|
||||
openssl genrsa -out eventrt_client_key.pem 2048
|
||||
|
||||
openssl req -new -key eventrt_client_key.pem \
|
||||
-out eventrt_client.csr -config eventrt.cnf
|
||||
|
||||
openssl x509 -req -in eventrt_client.csr \
|
||||
-CA ca_certificate.pem -CAkey cakey.pem -CAcreateserial \
|
||||
-out eventrt_client_cert.pem -days 365 \
|
||||
-extensions v3_client -extfile eventrt.cnf
|
||||
|
||||
rm eventrt_client.csr
|
||||
```
|
||||
|
||||
##### 4.2.5 验证证书
|
||||
|
||||
```bash
|
||||
# 验证服务器证书
|
||||
openssl verify -CAfile ca_certificate.pem server_certificate.pem
|
||||
|
||||
# 验证客户端证书
|
||||
openssl verify -CAfile ca_certificate.pem modelrt_client_cert.pem
|
||||
openssl verify -CAfile ca_certificate.pem eventrt_client_cert.pem
|
||||
|
||||
# 查看证书详情(确认 CN 和 SAN)
|
||||
openssl x509 -in server_certificate.pem -noout -subject -ext subjectAltName
|
||||
openssl x509 -in modelrt_client_cert.pem -noout -subject
|
||||
openssl x509 -in eventrt_client_cert.pem -noout -subject
|
||||
```
|
||||
|
||||
#### 4.3 部署 RabbitMQ
|
||||
|
||||
##### 4.3.1 创建证书 Secret
|
||||
|
||||
将服务器端三个证书文件打包为 K8s Secret(在证书文件所在目录执行):
|
||||
|
||||
```bash
|
||||
sh deploy/k8s/rabbitmq-certs-secret.sh
|
||||
```
|
||||
|
||||
该脚本等价于:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic rabbitmq-certs \
|
||||
--from-file=ca_certificate.pem=./ca_certificate.pem \
|
||||
--from-file=server_certificate.pem=./server_certificate.pem \
|
||||
--from-file=server_key.pem=./server_key.pem
|
||||
```
|
||||
|
||||
##### 4.3.2 部署
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/rabbitmq-secret.yaml
|
||||
kubectl apply -f deploy/k8s/rabbitmq-config.yaml
|
||||
kubectl apply -f deploy/k8s/rabbitmq-users-config.yaml
|
||||
kubectl apply -f deploy/k8s/rabbitmq-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/rabbitmq-service.yaml
|
||||
```
|
||||
|
||||
##### 4.3.3 端口汇总
|
||||
|
||||
| 端口 | NodePort | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `5671` | `30671` | AMQP over TLS(客户端连接) |
|
||||
| `5672` | `30672` | AMQP 明文(内部备用,生产禁用) |
|
||||
| `15671` | `31671` | Management UI over TLS |
|
||||
| `15672` | `31672` | Management UI 明文(内部备用) |
|
||||
|
||||
##### 4.3.4 用户与权限说明
|
||||
|
||||
用户定义在 `rabbitmq-users-config.yaml` 的 `definitions.json` 中,通过 `load_definitions` 启动时自动加载:
|
||||
|
||||
| 用户 | 认证方式 | 权限 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `coslight` | 密码 | administrator | 管理员,密码在 rabbitmq-secret.yaml |
|
||||
| `modelrt-client` | X.509 证书(CN) | configure/read/write | ModelRT 服务专用 |
|
||||
| `eventrt-client` | X.509 证书(CN) | configure/read/write | EventRT 服务专用 |
|
||||
| `web-client` | X.509 证书(CN) | read/write | Web 客户端 |
|
||||
|
||||
> **注意:** 证书认证用户的 `password_hash` 留空;RabbitMQ 通过 `ssl_cert_login_from = common_name` 将证书 CN 映射为用户名。
|
||||
|
||||
#### 4.4 部署 PostgreSQL
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/pg-configmap.yaml
|
||||
kubectl apply -f deploy/k8s/pg-pvc.yaml
|
||||
kubectl apply -f deploy/k8s/pg-statefulset.yaml
|
||||
kubectl apply -f deploy/k8s/pg-service.yaml
|
||||
```
|
||||
|
||||
| 参数 | 值 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **镜像** | `postgres:13.16` | PostgreSQL 13.16 |
|
||||
| **NodePort** | `30432` | 集群外访问端口 |
|
||||
| **数据库** | `demo` | ConfigMap 中 `POSTGRES_DB` |
|
||||
| **用户名** | `postgres` | ConfigMap 中 `POSTGRES_USER` |
|
||||
| **密码** | `coslight` | ConfigMap `postgres-config` 中配置,生产环境迁移至 Secret |
|
||||
| **存储** | `6Gi` | PVC `postgres-data` |
|
||||
| **CPU** | `100m` 请求 / `500m` 上限 | StatefulSet `resources` 字段 |
|
||||
| **内存** | `256Mi` 请求 / `512Mi` 上限 | StatefulSet `resources` 字段 |
|
||||
|
||||
> **注意:** 密码当前以明文形式存储在 `pg-configmap.yaml` 中,生产环境应将其迁移至 K8s Secret,并通过环境变量注入容器,避免将明文密码提交至版本库。
|
||||
|
||||
##### 4.4.1 等待 Pod 就绪
|
||||
|
||||
```bash
|
||||
kubectl wait --for=condition=ready pod -l app=postgres --timeout=120s
|
||||
```
|
||||
|
||||
##### 4.4.2 连接验证
|
||||
|
||||
```bash
|
||||
# 快速检查 PostgreSQL 是否接受连接
|
||||
kubectl exec -it $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
|
||||
-- pg_isready -U postgres -d demo
|
||||
|
||||
# 进入 psql 执行简单查询确认数据库可用
|
||||
kubectl exec -it $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
|
||||
-- psql -U postgres -d demo -c "SELECT current_database(), version();"
|
||||
|
||||
# 列出所有数据库(确认 demo 库已创建)
|
||||
kubectl exec -it $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
|
||||
-- psql -U postgres -c "\l"
|
||||
```
|
||||
|
||||
##### 4.4.3 初始化异步任务表
|
||||
|
||||
PostgreSQL 就绪后执行 1.4 节的建表 SQL,可通过以下方式进入容器执行:
|
||||
|
||||
```bash
|
||||
# 交互式 psql
|
||||
kubectl exec -it $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
|
||||
-- psql -U postgres -d demo
|
||||
|
||||
# 或将 SQL 文件通过管道一次性执行
|
||||
kubectl exec -i $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
|
||||
-- psql -U postgres -d demo < /path/to/init.sql
|
||||
```
|
||||
|
||||
##### 4.4.4 状态检查
|
||||
|
||||
```bash
|
||||
kubectl get pods -l app=postgres
|
||||
kubectl logs -l app=postgres --tail=30
|
||||
```
|
||||
|
||||
##### 4.4.5 清理
|
||||
|
||||
```bash
|
||||
kubectl delete -f deploy/k8s/pg-service.yaml \
|
||||
-f deploy/k8s/pg-statefulset.yaml \
|
||||
-f deploy/k8s/pg-pvc.yaml \
|
||||
-f deploy/k8s/pg-configmap.yaml
|
||||
```
|
||||
|
||||
### 5\. 部署 ModelRT(Kubernetes)
|
||||
|
||||
所有资源部署在 `default` 命名空间,YAML 文件位于 `deploy/k8s/`。
|
||||
|
||||
#### 5.1 构建并推送镜像
|
||||
|
||||
镜像采用三阶段构建,最终基于 `scratch`:
|
||||
|
||||
| 阶段 | 基础镜像 | 作用 |
|
||||
| :--- | :--- | :--- |
|
||||
| **builder** | `golang:1.26-alpine` | 编译 Go 二进制(`CGO_ENABLED=0`,`-trimpath -ldflags="-s -w"`) |
|
||||
| **certs** | `alpine:3.21` | 提取 CA 证书、时区数据及非 root 用户定义(UID 默认 `1000`) |
|
||||
| **runtime** | `scratch` | 仅含可执行文件与运行时依赖,无 shell、无包管理器 |
|
||||
|
||||
**方式一:从源码构建并加载**
|
||||
|
||||
```bash
|
||||
# 在项目根目录执行(默认运行用户 UID=1000)
|
||||
docker build -f deploy/dockerfile/modelrt.Dockerfile -t coslight/modelrt:latest .
|
||||
|
||||
# 自定义运行用户 UID
|
||||
docker build -f deploy/dockerfile/modelrt.Dockerfile \
|
||||
--build-arg USER_ID=2000 \
|
||||
-t coslight/modelrt:latest .
|
||||
|
||||
# 加载到 Minikube(无需私有仓库)
|
||||
minikube image load coslight/modelrt:latest
|
||||
```
|
||||
|
||||
**方式二:直接加载已有本地镜像**
|
||||
|
||||
Ubuntu 宿主机上已存在构建好的镜像(如 `modelrt:v1`)时,无需重新构建,直接导入 Minikube:
|
||||
|
||||
```bash
|
||||
# 确认本地镜像存在
|
||||
docker images modelrt:v1
|
||||
|
||||
# 加载到 Minikube
|
||||
minikube image load modelrt:v1
|
||||
|
||||
# 验证镜像已进入 Minikube 缓存
|
||||
minikube image ls | grep modelrt
|
||||
```
|
||||
|
||||
> **注意:** `deploy/k8s/modelrt-deployment.yaml` 中的 `image` 字段需与加载的镜像名称一致,并将 `imagePullPolicy` 设为 `Never`,防止 Minikube 尝试从远端拉取。
|
||||
|
||||
#### 5.1.1 镜像冒烟测试
|
||||
|
||||
```bash
|
||||
# 查看镜像大小(scratch 镜像预期 ≤ 25 MB)
|
||||
docker images coslight/modelrt:latest
|
||||
|
||||
# 检查镜像元信息(确认 User、Cmd、架构)
|
||||
docker inspect coslight/modelrt:latest
|
||||
|
||||
# 验证二进制可执行(无 config 时程序报错退出属预期行为,说明镜像构建正常)
|
||||
docker run --rm coslight/modelrt:latest
|
||||
|
||||
# 挂载示例配置做完整启动验证(Ctrl+C 退出)
|
||||
docker run --rm \
|
||||
-v "$(pwd)/configs/config.example.yaml:/app/configs/config.yaml" \
|
||||
-p 8080:8080 \
|
||||
coslight/modelrt:latest
|
||||
```
|
||||
|
||||
> **注意:** `scratch` 镜像不含 shell,无法使用 `docker exec` 进入容器调试;如需排查问题,可临时将最终阶段改为 `alpine` 进行本地调试,确认后再切回 `scratch`。
|
||||
|
||||
#### 5.2 创建客户端证书 Secret
|
||||
|
||||
在 RabbitMQ TLS 证书生成完成后(见 4.2),进入证书文件所在目录执行:
|
||||
|
||||
```bash
|
||||
sh deploy/k8s/modelrt-certs-secret.sh
|
||||
```
|
||||
|
||||
该脚本等价于:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic modelrt-certs \
|
||||
--from-file=ca_certificate.pem=./ca_certificate.pem \
|
||||
--from-file=modelrt_client_cert.pem=./modelrt_client_cert.pem \
|
||||
--from-file=modelrt_client_key.pem=./modelrt_client_key.pem
|
||||
```
|
||||
|
||||
#### 5.3 部署
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/modelrt-secret.yaml
|
||||
kubectl apply -f deploy/k8s/modelrt-configmap.yaml
|
||||
kubectl apply -f deploy/k8s/modelrt-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/modelrt-service.yaml
|
||||
```
|
||||
|
||||
#### 5.4 配置说明
|
||||
|
||||
| 配置项 | 方式 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `postgres.password` | Secret `modelrt-secret` | 不写入 ConfigMap |
|
||||
| `service.secret_key` | Secret `modelrt-secret` | 不写入 ConfigMap |
|
||||
| RabbitMQ 客户端证书 | Secret `modelrt-certs` | 挂载至 `/app/configs/certs/` |
|
||||
| `config.yaml` 其余配置 | ConfigMap `modelrt-config` | 所有 host 已替换为 K8s service 名 |
|
||||
| `K8S_NAMESPACE` / `K8S_NODE_NAME` | Downward API | 注入至日志全局字段 |
|
||||
|
||||
> **注意:** `modelrt-configmap.yaml` 中 `postgres.password` 和 `service.secret_key` 留空,实际值由容器启动时的环境变量 `POSTGRES_PASSWORD` / `SERVICE_SECRET_KEY` 注入,应用需读取这两个环境变量覆盖 config 中的空值。若应用当前仅读取文件配置,可直接将值填入 `modelrt-secret.yaml` 并在 ConfigMap 中引用,或在 ConfigMap 中直接填写。
|
||||
|
||||
#### 5.5 状态检查
|
||||
|
||||
```bash
|
||||
# 查看 Pod 状态
|
||||
kubectl get pods -l app=modelrt
|
||||
|
||||
# 查看启动日志
|
||||
kubectl logs -l app=modelrt --tail=50
|
||||
|
||||
# 查看 Service
|
||||
kubectl get svc modelrt-service
|
||||
```
|
||||
|
||||
#### 5.6 端口汇总
|
||||
|
||||
| NodePort | 说明 |
|
||||
| :--- | :--- |
|
||||
| `30080` | ModelRT HTTP API,SSH 隧道本地端口 `8080` |
|
||||
|
||||
#### 5.7 清理
|
||||
|
||||
```bash
|
||||
kubectl delete -f deploy/k8s/modelrt-service.yaml \
|
||||
-f deploy/k8s/modelrt-deployment.yaml \
|
||||
-f deploy/k8s/modelrt-configmap.yaml \
|
||||
-f deploy/k8s/modelrt-secret.yaml
|
||||
kubectl delete secret modelrt-certs
|
||||
```
|
||||
|
||||
### 6\. 部署可观测性栈(Kubernetes)
|
||||
|
||||
在 `Kubernetes` 集群中部署 `Jaeger`(链路追踪)+ `Loki + Alloy + Grafana`(日志可视化)。所有资源部署在 `default` 命名空间,`YAML` 文件位于 `deploy/k8s/`。
|
||||
|
||||
> **日志采集器说明:** 集群内的日志采集由 `Grafana Alloy`(DaemonSet)负责,它通过 Kubernetes API 抓取带 `app` label 的 Pod 容器日志,解析 `zap` 输出的 JSON 字段后推送到 `Loki`。Alloy 已**替代**早期的 `Promtail`,两者推送目标(`loki-service:3100`)与标签解析完全一致,**不要同时部署**,否则会导致 Loki 中日志翻倍。
|
||||
|
||||
#### 6.1 部署 Jaeger
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/jaeger-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/jaeger-service.yaml
|
||||
```
|
||||
|
||||
#### 6.2 部署 Loki
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/loki-configmap.yaml
|
||||
kubectl apply -f deploy/k8s/loki-pvc.yaml
|
||||
kubectl apply -f deploy/k8s/loki-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/loki-service.yaml
|
||||
```
|
||||
|
||||
#### 6.3 部署 Alloy
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/alloy-rbac.yaml
|
||||
kubectl apply -f deploy/k8s/alloy-configmap.yaml
|
||||
kubectl apply -f deploy/k8s/alloy-daemonset.yaml
|
||||
```
|
||||
|
||||
> Alloy 以 DaemonSet 形式在每个节点运行,需要 `ServiceAccount` + `ClusterRole`(`alloy-rbac.yaml`)授予读取 `nodes/pods/pods/log` 的权限。采集与解析规则定义在 `alloy-configmap.yaml` 的 `config.alloy` 中。
|
||||
|
||||
#### 6.4 部署 Grafana
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/grafana-configmap.yaml
|
||||
kubectl apply -f deploy/k8s/grafana-deployment.yaml
|
||||
kubectl apply -f deploy/k8s/grafana-service.yaml
|
||||
```
|
||||
|
||||
#### 6.5 一键部署
|
||||
|
||||
```bash
|
||||
kubectl apply -f deploy/k8s/jaeger-deployment.yaml \
|
||||
-f deploy/k8s/jaeger-service.yaml \
|
||||
-f deploy/k8s/loki-configmap.yaml \
|
||||
-f deploy/k8s/loki-pvc.yaml \
|
||||
-f deploy/k8s/loki-deployment.yaml \
|
||||
-f deploy/k8s/loki-service.yaml \
|
||||
-f deploy/k8s/alloy-rbac.yaml \
|
||||
-f deploy/k8s/alloy-configmap.yaml \
|
||||
-f deploy/k8s/alloy-daemonset.yaml \
|
||||
-f deploy/k8s/grafana-configmap.yaml \
|
||||
-f deploy/k8s/grafana-deployment.yaml \
|
||||
-f deploy/k8s/grafana-service.yaml
|
||||
```
|
||||
|
||||
#### 6.6 状态检查
|
||||
|
||||
```bash
|
||||
# 查看所有 Pod 状态
|
||||
kubectl get pods
|
||||
|
||||
# 查看所有 Service 及 NodePort
|
||||
kubectl get svc
|
||||
```
|
||||
|
||||
#### 6.7 端口汇总
|
||||
|
||||
| 服务 | NodePort | 访问地址 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Jaeger UI** | `31686` | `http://<NodeIP>:31686` | 链路追踪查询界面 |
|
||||
| **Loki** | `31100` | `http://<NodeIP>:31100` | 日志 HTTP API |
|
||||
| **Grafana** | `31000` | `http://<NodeIP>:31000` | 可视化界面,账号 `admin / coslight` |
|
||||
| **OTLP gRPC** | `31317` | `<NodeIP>:31317` | ModelRT OTel 上报地址(gRPC) |
|
||||
| **OTLP HTTP** | `31318` | `http://<NodeIP>:31318` | ModelRT OTel 上报地址(HTTP) |
|
||||
|
||||
#### 6.8 清理
|
||||
|
||||
```bash
|
||||
kubectl delete -f deploy/k8s/
|
||||
```
|
||||
|
||||
### 7\. Mac 本地访问(SSH 隧道)
|
||||
|
||||
`ModelRT / EventRT` 在 `Mac` 本地运行时,依赖的 `RabbitMQ`、`Redis`、`Jaeger`、`Loki`、`Grafana` 均部署在 `Ubuntu` 宿主机(`192.168.1.101`)上的 `Minikube`(`192.168.49.2`)中。由于 `Minikube` 网络不直接对外暴露,需通过 `SSH` 本地端口转发建立访问隧道。
|
||||
|
||||
#### 7.1 网络拓扑
|
||||
|
||||
``` text
|
||||
Mac 本地端口 ──SSH隧道──▶ Ubuntu 宿主机 (192.168.1.101) ──▶ Minikube NodePort (192.168.49.2)
|
||||
```
|
||||
|
||||
#### 7.2 建立隧道
|
||||
|
||||
```bash
|
||||
ssh -L 5432:192.168.49.2:30432 \
|
||||
-L 5671:192.168.49.2:30671 \
|
||||
-L 15671:192.168.49.2:31671 \
|
||||
-L 6379:192.168.49.2:30001 \
|
||||
-L 4318:192.168.49.2:31318 \
|
||||
-L 16686:192.168.49.2:31686 \
|
||||
-L 3100:192.168.49.2:31100 \
|
||||
-L 3000:192.168.49.2:31000 \
|
||||
douxu@192.168.1.101
|
||||
```
|
||||
|
||||
如需后台静默运行(不占用终端):
|
||||
|
||||
```bash
|
||||
ssh -fN \
|
||||
-L 5432:192.168.49.2:30432 \
|
||||
-L 5671:192.168.49.2:30671 \
|
||||
-L 15671:192.168.49.2:31671 \
|
||||
-L 6379:192.168.49.2:30001 \
|
||||
-L 4318:192.168.49.2:31318 \
|
||||
-L 16686:192.168.49.2:31686 \
|
||||
-L 3100:192.168.49.2:31100 \
|
||||
-L 3000:192.168.49.2:31000 \
|
||||
douxu@192.168.1.101
|
||||
```
|
||||
|
||||
#### 7.3 端口映射说明
|
||||
|
||||
| Mac 本地端口 | Minikube NodePort | 服务 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `5432` | `30432` | PostgreSQL | 数据库连接 `localhost:5432` |
|
||||
| `5671` | `30671` | RabbitMQ AMQP | ModelRT / EventRT 消息队列连接 |
|
||||
| `15671` | `31671` | RabbitMQ Management | RabbitMQ 管理界面 `http://localhost:15671` |
|
||||
| `6379` | `30001` | Redis | 分布式锁 / 数据存储 |
|
||||
| `4318` | `31318` | OTLP HTTP | OTel Trace 上报(Jaeger Collector) |
|
||||
| `16686` | `31686` | Jaeger UI | 链路追踪查询 `http://localhost:16686` |
|
||||
| `3100` | `31100` | Loki | 日志查询 API |
|
||||
| `3000` | `31000` | Grafana | 可视化界面 `http://localhost:3000` |
|
||||
|
||||
> **注意:** 隧道建立后,本地配置文件中所有服务地址均填 `localhost:<本地端口>`,无需修改即可在 `Mac` 上直接运行服务。
|
||||
|
||||
#### 7.4 关闭隧道
|
||||
|
||||
前台运行时直接 `Ctrl+C`;后台运行时查找并终止进程:
|
||||
|
||||
```bash
|
||||
# 找到 ssh 隧道进程
|
||||
ps aux | grep "ssh -fN"
|
||||
# 终止(替换为实际 PID)
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 8\. 后续操作(停止与清理)
|
||||
|
||||
#### 8.1 本地 Docker 部署清理
|
||||
|
||||
适用于第 1、2 节使用 `docker run` 启动的 PostgreSQL 和 Redis 容器。
|
||||
|
||||
```bash
|
||||
# 停止容器
|
||||
docker stop postgres redis
|
||||
```
|
||||
|
||||
# 删除容器(容器内数据将同步丢失)
|
||||
#### 4.2 删除容器(删除后数据将丢失)
|
||||
|
||||
```bash
|
||||
docker rm postgres redis
|
||||
```
|
||||
|
||||
#### 8.2 本地运行清理
|
||||
|
||||
适用于第 3 节以 `go run` 或编译后二进制方式在本地启动的 ModelRT 服务。
|
||||
|
||||
前台运行时直接 `Ctrl+C` 终止;后台运行时查找并终止进程:
|
||||
|
||||
```bash
|
||||
# 终止 go run 启动的进程
|
||||
pkill -f "go run main.go"
|
||||
|
||||
# 或终止编译后的二进制进程
|
||||
pkill model-rt
|
||||
```
|
||||
|
||||
#### 8.3 K8s(Minikube) 部署清理
|
||||
|
||||
适用于第 4、5、6 节在 Minikube 中部署的所有资源。
|
||||
|
||||
##### 8.3.1 分服务清理
|
||||
|
||||
**仅停止(缩容至 0,PVC 数据保留)**
|
||||
|
||||
将所有 Deployment 和 StatefulSet 缩容至 0 副本,Pod 停止运行但持久卷数据不删除,之后可直接缩容回 1 恢复服务。
|
||||
|
||||
```bash
|
||||
# 停止所有 Deployment(Redis / RabbitMQ / ModelRT / Jaeger / Loki / Grafana)
|
||||
kubectl scale deployment --all --replicas=0
|
||||
|
||||
# 停止所有 StatefulSet(PostgreSQL,PVC 数据保留)
|
||||
kubectl scale statefulset --all --replicas=0
|
||||
```
|
||||
|
||||
恢复时:
|
||||
|
||||
```bash
|
||||
kubectl scale deployment --all --replicas=1
|
||||
kubectl scale statefulset --all --replicas=1
|
||||
```
|
||||
|
||||
> **注意:** DaemonSet(Alloy)无法通过 `scale` 停止,如需停用可手动删除其资源:`kubectl delete -f deploy/k8s/alloy-daemonset.yaml`。
|
||||
|
||||
---
|
||||
|
||||
**永久清理(删除所有资源,包含 PVC,数据不可恢复)**
|
||||
|
||||
按部署顺序反向删除各服务资源:
|
||||
|
||||
```bash
|
||||
# 可观测性栈(Grafana / Alloy / Loki / Jaeger)
|
||||
kubectl delete -f deploy/k8s/grafana-service.yaml \
|
||||
-f deploy/k8s/grafana-deployment.yaml \
|
||||
-f deploy/k8s/grafana-configmap.yaml \
|
||||
-f deploy/k8s/alloy-daemonset.yaml \
|
||||
-f deploy/k8s/alloy-configmap.yaml \
|
||||
-f deploy/k8s/alloy-rbac.yaml \
|
||||
-f deploy/k8s/loki-service.yaml \
|
||||
-f deploy/k8s/loki-deployment.yaml \
|
||||
-f deploy/k8s/loki-pvc.yaml \
|
||||
-f deploy/k8s/loki-configmap.yaml \
|
||||
-f deploy/k8s/jaeger-service.yaml \
|
||||
-f deploy/k8s/jaeger-deployment.yaml
|
||||
|
||||
# ModelRT 应用
|
||||
kubectl delete -f deploy/k8s/modelrt-service.yaml \
|
||||
-f deploy/k8s/modelrt-deployment.yaml \
|
||||
-f deploy/k8s/modelrt-configmap.yaml \
|
||||
-f deploy/k8s/modelrt-secret.yaml
|
||||
kubectl delete secret modelrt-certs
|
||||
|
||||
# PostgreSQL
|
||||
kubectl delete -f deploy/k8s/pg-service.yaml \
|
||||
-f deploy/k8s/pg-statefulset.yaml \
|
||||
-f deploy/k8s/pg-pvc.yaml \
|
||||
-f deploy/k8s/pg-configmap.yaml
|
||||
|
||||
# RabbitMQ
|
||||
kubectl delete -f deploy/k8s/rabbitmq-service.yaml \
|
||||
-f deploy/k8s/rabbitmq-deployment.yaml \
|
||||
-f deploy/k8s/rabbitmq-users-config.yaml \
|
||||
-f deploy/k8s/rabbitmq-config.yaml \
|
||||
-f deploy/k8s/rabbitmq-secret.yaml
|
||||
kubectl delete secret rabbitmq-certs
|
||||
|
||||
# Redis
|
||||
kubectl delete -f deploy/k8s/redis-service.yaml \
|
||||
-f deploy/k8s/redis-deployment.yaml
|
||||
```
|
||||
|
||||
##### 8.3.2 一键清理
|
||||
|
||||
> **注意:** 此操作会删除 `deploy/k8s/` 下所有 YAML 对应的 K8s 资源,包括 PVC,**持久化数据将永久丢失**,请确认后执行。
|
||||
|
||||
```bash
|
||||
kubectl delete -f deploy/k8s/
|
||||
kubectl delete secret rabbitmq-certs modelrt-certs
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,34 +1,19 @@
|
|||
FROM golang:1.26-alpine AS builder
|
||||
RUN apk --no-cache upgrade
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN GOPROXY="https://goproxy.cn,direct" go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-mod=readonly \
|
||||
-o modelrt main.go
|
||||
|
||||
# prepare runtime dependencies in a pinned alpine stage so they can be
|
||||
# copied into scratch without pulling any vulnerable os packages at run time.
|
||||
FROM alpine:3.21 AS certs
|
||||
ARG USER_ID=1000
|
||||
RUN apk --no-cache add ca-certificates tzdata && \
|
||||
adduser -D -u ${USER_ID} modelrt
|
||||
|
||||
FROM scratch
|
||||
# CA certificates required for TLS connections (RabbitMQ amqps://)
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
# timezone data
|
||||
COPY --from=certs /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
# non-root user/group definitions
|
||||
COPY --from=certs /etc/passwd /etc/passwd
|
||||
COPY --from=certs /etc/group /etc/group
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o modelrt main.go
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
ARG USER_ID=1000
|
||||
RUN adduser -D -u ${USER_ID} modelrt
|
||||
COPY --from=builder /app/modelrt ./modelrt
|
||||
|
||||
COPY configs/config.example.yaml ./configs/config.example.yaml
|
||||
RUN chown -R modelrt:modelrt /app
|
||||
RUN chmod +x /app/modelrt
|
||||
USER modelrt
|
||||
CMD ["/app/modelrt", "-modelRT_config_dir=/app/configs"]
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: alloy-config
|
||||
namespace: default
|
||||
data:
|
||||
config.alloy: |
|
||||
// 发现集群内所有 Pod
|
||||
discovery.kubernetes "pods" {
|
||||
role = "pod"
|
||||
}
|
||||
|
||||
// 重写元数据标签,并只保留带 app label 的 Pod
|
||||
discovery.relabel "pods" {
|
||||
targets = discovery.kubernetes.pods.targets
|
||||
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_namespace"]
|
||||
target_label = "namespace"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_name"]
|
||||
target_label = "pod"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_container_name"]
|
||||
target_label = "container"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_label_app"]
|
||||
target_label = "app"
|
||||
}
|
||||
// 只采集有 app label 的 Pod
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_label_app"]
|
||||
action = "keep"
|
||||
regex = ".+"
|
||||
}
|
||||
}
|
||||
|
||||
// 通过 Kubernetes API 抓取容器日志(无需挂载宿主机日志目录)
|
||||
loki.source.kubernetes "pods" {
|
||||
targets = discovery.relabel.pods.output
|
||||
forward_to = [loki.process.parse.receiver]
|
||||
}
|
||||
|
||||
// 解析 zap 输出的 JSON 日志,并将关键字段提升为 Loki Label
|
||||
loki.process "parse" {
|
||||
forward_to = [loki.write.default.receiver]
|
||||
|
||||
// 解析结构化字段
|
||||
stage.json {
|
||||
expressions = {
|
||||
level = "level",
|
||||
traceID = "traceID",
|
||||
spanID = "spanID",
|
||||
caller = "caller",
|
||||
pod = "pod",
|
||||
namespace = "namespace",
|
||||
node = "node",
|
||||
}
|
||||
}
|
||||
|
||||
// 提升为 Label,支持在 Grafana 中按实例/Trace 过滤
|
||||
stage.labels {
|
||||
values = {
|
||||
level = "",
|
||||
traceID = "",
|
||||
pod = "",
|
||||
namespace = "",
|
||||
node = "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 推送到 Loki
|
||||
loki.write "default" {
|
||||
endpoint {
|
||||
url = "http://loki-service:3100/loki/api/v1/push"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: alloy
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: alloy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: alloy
|
||||
spec:
|
||||
serviceAccountName: alloy
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: alloy
|
||||
image: grafana/alloy:v1.16.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- run
|
||||
- /etc/alloy/config.alloy
|
||||
- --storage.path=/var/lib/alloy/data
|
||||
- --server.http.listen-addr=0.0.0.0:12345
|
||||
ports:
|
||||
- containerPort: 12345
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/alloy
|
||||
- name: data
|
||||
mountPath: /var/lib/alloy/data
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: alloy-config
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: alloy
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: alloy
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes", "nodes/proxy", "services", "endpoints", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: alloy
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: alloy
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: alloy
|
||||
namespace: default
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: grafana-datasources
|
||||
namespace: default
|
||||
data:
|
||||
datasources.yaml: |
|
||||
apiVersion: 1
|
||||
datasources:
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki-service:3100
|
||||
isDefault: true
|
||||
jsonData:
|
||||
# derivedFields: 从日志的 traceID 字段生成跳转链接到 Jaeger
|
||||
derivedFields:
|
||||
- matcherRegex: '"traceID":\s*"([a-f0-9]+)"'
|
||||
name: TraceID
|
||||
url: http://127.0.0.1:16686/trace/$${__value.raw}
|
||||
targetBlank: true
|
||||
- name: Jaeger
|
||||
type: jaeger
|
||||
uid: jaeger
|
||||
access: proxy
|
||||
url: http://jaeger-service:16686
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: grafana
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: grafana
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: grafana
|
||||
spec:
|
||||
containers:
|
||||
- name: grafana
|
||||
image: grafana/grafana:10.4.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: GF_SECURITY_ADMIN_USER
|
||||
value: "coslight"
|
||||
- name: GF_SECURITY_ADMIN_PASSWORD
|
||||
value: "coslight@tj"
|
||||
- name: GF_AUTH_ANONYMOUS_ENABLED
|
||||
value: "false"
|
||||
volumeMounts:
|
||||
- name: datasources
|
||||
mountPath: /etc/grafana/provisioning/datasources
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
volumes:
|
||||
- name: datasources
|
||||
configMap:
|
||||
name: grafana-datasources
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: grafana-service
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
nodePort: 31000 # Grafana UI: http://<NodeIP>:31000
|
||||
selector:
|
||||
app: grafana
|
||||
type: NodePort
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jaeger
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jaeger
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jaeger
|
||||
spec:
|
||||
containers:
|
||||
- name: jaeger
|
||||
image: jaegertracing/all-in-one:1.56
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: COLLECTOR_OTLP_ENABLED
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 16686 # UI
|
||||
- containerPort: 14268 # Jaeger Collector
|
||||
- containerPort: 4317 # OTLP gRPC
|
||||
- containerPort: 4318 # OTLP HTTP
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jaeger-service
|
||||
labels:
|
||||
app: jaeger
|
||||
spec:
|
||||
ports:
|
||||
- name: ui
|
||||
port: 16686
|
||||
targetPort: 16686
|
||||
nodePort: 31686 # Jaeger UI,浏览器访问 http://<NodeIP>:31686
|
||||
- name: collector-http
|
||||
port: 14268
|
||||
targetPort: 14268
|
||||
nodePort: 31268 # Jaeger 原生 HTTP collector(非 OTel)
|
||||
- name: otlp-http
|
||||
port: 4318
|
||||
targetPort: 4318
|
||||
nodePort: 31318 # OTLP HTTP,集群外使用 <NodeIP>:31318
|
||||
- name: otlp-grpc
|
||||
port: 4317
|
||||
targetPort: 4317
|
||||
nodePort: 31317 # OTLP gRPC,集群外使用 <NodeIP>:31317
|
||||
selector:
|
||||
app: jaeger
|
||||
type: NodePort
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: loki-config
|
||||
namespace: default
|
||||
data:
|
||||
loki.yaml: |
|
||||
auth_enabled: false
|
||||
|
||||
server:
|
||||
http_listen_port: 3100
|
||||
|
||||
ingester:
|
||||
wal:
|
||||
enabled: true
|
||||
dir: /loki/wal # 指向 PVC 挂载路径,避免在容器根目录创建 /wal 时 permission denied
|
||||
lifecycler:
|
||||
ring:
|
||||
kvstore:
|
||||
store: inmemory
|
||||
replication_factor: 1
|
||||
chunk_idle_period: 5m
|
||||
chunk_retain_period: 30s
|
||||
|
||||
schema_config:
|
||||
configs:
|
||||
- from: 2024-01-01
|
||||
store: boltdb-shipper
|
||||
object_store: filesystem
|
||||
schema: v11
|
||||
index:
|
||||
prefix: index_
|
||||
period: 24h
|
||||
|
||||
storage_config:
|
||||
boltdb_shipper:
|
||||
active_index_directory: /loki/index
|
||||
cache_location: /loki/cache
|
||||
shared_store: filesystem
|
||||
filesystem:
|
||||
directory: /loki/chunks
|
||||
|
||||
limits_config:
|
||||
reject_old_samples: true
|
||||
reject_old_samples_max_age: 168h
|
||||
|
||||
compactor:
|
||||
working_directory: /loki/compactor
|
||||
shared_store: filesystem
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: loki
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: loki
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: loki
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 10001 # 使 PVC 挂载目录对 Loki 默认用户(UID 10001)可写
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
containers:
|
||||
- name: loki
|
||||
image: grafana/loki:2.9.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/loki/loki.yaml
|
||||
ports:
|
||||
- containerPort: 3100
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/loki
|
||||
- name: storage
|
||||
mountPath: /loki
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: loki-config
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: loki-pvc
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: loki-pvc
|
||||
namespace: default
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: loki-service
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 3100
|
||||
targetPort: 3100
|
||||
nodePort: 31100 # 集群外访问: http://<NodeIP>:31100
|
||||
selector:
|
||||
app: loki
|
||||
type: NodePort
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/bin/sh
|
||||
# Create the modelrt client certificate secret.
|
||||
# Run this script from the directory that contains the three cert files,
|
||||
# or adjust the paths below to point at the actual files.
|
||||
#
|
||||
# Expected files (generated during RabbitMQ TLS setup):
|
||||
# ca_certificate.pem
|
||||
# modelrt_client_cert.pem
|
||||
# modelrt_client_key.pem
|
||||
|
||||
kubectl create secret generic modelrt-certs \
|
||||
--from-file=ca_certificate.pem=./ca_certificate.pem \
|
||||
--from-file=modelrt_client_cert.pem=./modelrt_client_cert.pem \
|
||||
--from-file=modelrt_client_key.pem=./modelrt_client_key.pem
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: modelrt-config
|
||||
data:
|
||||
config.yaml: |
|
||||
postgres:
|
||||
host: "postgres-service"
|
||||
port: 5432
|
||||
database: "demo"
|
||||
user: "postgres"
|
||||
password: "" # injected via env POSTGRES_PASSWORD
|
||||
|
||||
rabbitmq:
|
||||
ca_cert_path: "/app/configs/certs/ca_certificate.pem"
|
||||
client_key_path: "/app/configs/certs/modelrt_client_key.pem"
|
||||
client_key_password: ""
|
||||
client_cert_path: "/app/configs/certs/modelrt_client_cert.pem"
|
||||
insecure_skip_verify: false
|
||||
server_name: "rabbitmq-server"
|
||||
user: ""
|
||||
password: ""
|
||||
host: "rabbitmq-service"
|
||||
port: 5671
|
||||
|
||||
logger:
|
||||
mode: "production"
|
||||
level: "info"
|
||||
filepath: ""
|
||||
maxsize: 100
|
||||
maxbackups: 5
|
||||
maxage: 30
|
||||
compress: false
|
||||
loki:
|
||||
endpoint: "" # Promtail handles log collection in K8s, direct push disabled
|
||||
|
||||
otel:
|
||||
endpoint: "jaeger-service:4318"
|
||||
insecure: true
|
||||
|
||||
ants:
|
||||
parse_concurrent_quantity: 10
|
||||
rtd_receive_concurrent_quantity: 10
|
||||
|
||||
async_task:
|
||||
worker_pool_size: 10
|
||||
queue_consumer_count: 2
|
||||
max_retry_count: 3
|
||||
retry_initial_delay: 1s
|
||||
retry_max_delay: 5m
|
||||
health_check_interval: 30s
|
||||
|
||||
locker_redis:
|
||||
addr: "redis-service:6379"
|
||||
password: ""
|
||||
db: 1
|
||||
poolsize: 50
|
||||
dial_timeout: 10
|
||||
read_timeout: 10
|
||||
write_timeout: 10
|
||||
|
||||
storage_redis:
|
||||
addr: "redis-service:6379"
|
||||
password: ""
|
||||
db: 0
|
||||
poolsize: 50
|
||||
dial_timeout: 10
|
||||
read_timeout: 10
|
||||
write_timeout: 10
|
||||
|
||||
base:
|
||||
grid_id: 1
|
||||
zone_id: 1
|
||||
station_id: 1
|
||||
|
||||
service:
|
||||
service_addr: ":8080"
|
||||
service_name: "modelRT"
|
||||
secret_key: "" # injected via env SERVICE_SECRET_KEY
|
||||
deploy_env: "development"
|
||||
|
||||
dataRT:
|
||||
host: "http://127.0.0.1"
|
||||
port: 8888
|
||||
polling_api: "datart/getPointData"
|
||||
polling_api_method: "GET"
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: modelrt
|
||||
labels:
|
||||
app: modelrt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: modelrt
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: modelrt
|
||||
spec:
|
||||
containers:
|
||||
- name: modelrt
|
||||
image: modelrt:v1
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ["/app/modelrt"]
|
||||
args:
|
||||
- "-modelRT_config_dir=/app/configs"
|
||||
- "-modelRT_config_name=config"
|
||||
- "-modelRT_config_type=yaml"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
# Downward API — injected into every log line by logger/zap.go containerFields()
|
||||
- name: K8S_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: K8S_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
# HOSTNAME is set automatically by K8s to the pod name
|
||||
# Sensitive values injected from Secret so they stay out of ConfigMap
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: modelrt-secret
|
||||
key: postgres-password
|
||||
- name: SERVICE_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: modelrt-secret
|
||||
key: secret-key
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/configs/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
- name: certs
|
||||
mountPath: /app/configs/certs
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: modelrt-config
|
||||
- name: certs
|
||||
secret:
|
||||
secretName: modelrt-certs
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: modelrt-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
postgres-password: "coslight"
|
||||
secret-key: "modelrt_key"
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: modelrt-service
|
||||
labels:
|
||||
app: modelrt
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: modelrt
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
nodePort: 30080
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongodb-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: mongodb-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: coslight
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongodb-service
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: mongodb
|
||||
ports:
|
||||
- name: mongodb
|
||||
port: 27017
|
||||
targetPort: 27017
|
||||
nodePort: 30017
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mongodb
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
serviceName: mongodb
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongodb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: mongo:7.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: mongodb
|
||||
containerPort: 27017
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: mongodb-secret
|
||||
volumeMounts:
|
||||
- name: mongodb-data
|
||||
mountPath: /data/db
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mongosh
|
||||
- --eval
|
||||
- "db.adminCommand('ping')"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 12
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mongosh
|
||||
- --eval
|
||||
- "db.adminCommand('ping')"
|
||||
initialDelaySeconds: 120
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 30
|
||||
failureThreshold: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
volumes:
|
||||
- name: mongodb-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mongodb-data
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-config
|
||||
data:
|
||||
POSTGRES_DB: demo
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: coslight
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 6Gi
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-service
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
nodePort: 30432
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
serviceName: postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:13.16
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: postgres-config
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||
initialDelaySeconds: 8
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 12
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: promtail-config
|
||||
namespace: default
|
||||
data:
|
||||
promtail.yaml: |
|
||||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
clients:
|
||||
- url: http://loki-service:3100/loki/api/v1/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: kubernetes-pods
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
pipeline_stages:
|
||||
# 解析 zap 输出的 JSON 日志,提取结构化字段
|
||||
- json:
|
||||
expressions:
|
||||
level: level
|
||||
traceID: traceID
|
||||
spanID: spanID
|
||||
caller: caller
|
||||
pod: pod
|
||||
namespace: namespace
|
||||
node: node
|
||||
# 将关键字段提升为 Loki Label,支持在 Grafana 中按实例/Trace 过滤
|
||||
- labels:
|
||||
level:
|
||||
traceID:
|
||||
pod:
|
||||
namespace:
|
||||
node:
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
target_label: namespace
|
||||
- source_labels: [__meta_kubernetes_pod_name]
|
||||
target_label: pod
|
||||
- source_labels: [__meta_kubernetes_pod_container_name]
|
||||
target_label: container
|
||||
- source_labels: [__meta_kubernetes_pod_label_app]
|
||||
target_label: app
|
||||
# 只采集有 app label 的 Pod
|
||||
- source_labels: [__meta_kubernetes_pod_label_app]
|
||||
action: keep
|
||||
regex: .+
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: promtail
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: promtail
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: promtail
|
||||
spec:
|
||||
serviceAccountName: promtail
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: promtail
|
||||
image: grafana/promtail:2.9.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/promtail/promtail.yaml
|
||||
ports:
|
||||
- containerPort: 9080
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/promtail
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
readOnly: true
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: promtail-config
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: promtail
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: promtail
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes", "nodes/proxy", "services", "endpoints", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: promtail
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: promtail
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: promtail
|
||||
namespace: default
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/bin/sh
|
||||
# Create the rabbitmq server certificate secret.
|
||||
# Run this script from the directory that contains the three cert files,
|
||||
# or adjust the paths below to point at the actual files.
|
||||
#
|
||||
# Expected files (generated during RabbitMQ TLS setup):
|
||||
# ca_certificate.pem
|
||||
# server_certificate.pem
|
||||
# server_key.pem
|
||||
|
||||
kubectl create secret generic rabbitmq-certs \
|
||||
--from-file=ca_certificate.pem=./ca_certificate.pem \
|
||||
--from-file=server_certificate.pem=./server_certificate.pem \
|
||||
--from-file=server_key.pem=./server_key.pem
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rabbitmq-config
|
||||
data:
|
||||
rabbitmq.conf: |
|
||||
# 确保允许PLAIN认证
|
||||
auth_mechanisms.1 = PLAIN
|
||||
auth_mechanisms.2 = AMQPLAIN
|
||||
auth_mechanisms.3 = EXTERNAL
|
||||
# 允许admin用户通过远程方式连接
|
||||
loopback_users.admin = false
|
||||
# 默认心跳和监听配置可在此扩展
|
||||
# 确定 ssl 连接时验证使用的用户名
|
||||
ssl_cert_login_from = common_name
|
||||
# 开启此项配置会导致只能通过TLS端口访问
|
||||
listeners.tcp = none
|
||||
listeners.ssl.default = 5671
|
||||
# default user config
|
||||
load_definitions = /etc/rabbitmq/definitions.json
|
||||
# ssl config
|
||||
ssl_options.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
|
||||
ssl_options.certfile = /etc/rabbitmq/certs/server_certificate.pem
|
||||
ssl_options.keyfile = /etc/rabbitmq/certs/server_key.pem
|
||||
ssl_options.verify = verify_peer
|
||||
ssl_options.fail_if_no_peer_cert = true
|
||||
# management config
|
||||
management.ssl.port = 15671
|
||||
management.ssl.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
|
||||
management.ssl.certfile = /etc/rabbitmq/certs/server_certificate.pem
|
||||
management.ssl.keyfile = /etc/rabbitmq/certs/server_key.pem
|
||||
management.ssl.verify = verify_peer
|
||||
management.ssl.fail_if_no_peer_cert = true
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rabbitmq
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rabbitmq
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rabbitmq
|
||||
spec:
|
||||
containers:
|
||||
- name: rabbitmq
|
||||
image: rabbitmq:4.1.1-management-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 4369
|
||||
- containerPort: 5671
|
||||
- containerPort: 5672 # AMQP
|
||||
- containerPort: 15671
|
||||
- containerPort: 15672 # Management UI
|
||||
- containerPort: 15691
|
||||
- containerPort: 15692
|
||||
- containerPort: 25672
|
||||
env:
|
||||
- name: RABBITMQ_DEFAULT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rabbitmq-secret
|
||||
key: rabbitmq-user
|
||||
- name: RABBITMQ_DEFAULT_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rabbitmq-secret
|
||||
key: rabbitmq-pass
|
||||
- name: RABBITMQ_ERLANG_COOKIE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: rabbitmq-secret
|
||||
key: erlang-cookie
|
||||
- name: RABBITMQ_DEFAULT_VHOST
|
||||
value: "/"
|
||||
volumeMounts:
|
||||
- name: rabbitmq-certs-volume
|
||||
mountPath: /etc/rabbitmq/certs
|
||||
readOnly: true
|
||||
- name: rabbitmq-config-volume
|
||||
mountPath: /etc/rabbitmq/rabbitmq.conf
|
||||
subPath: rabbitmq.conf
|
||||
- name: rabbitmq-config-volume
|
||||
mountPath: /etc/rabbitmq/advanced.config
|
||||
subPath: advanced.config
|
||||
readOnly: true
|
||||
- name: plugins-config-volume
|
||||
mountPath: /etc/rabbitmq/enabled_plugins
|
||||
subPath: enabled_plugins
|
||||
- name: users-config-volume
|
||||
mountPath: /etc/rabbitmq/definitions.json
|
||||
subPath: definitions.json
|
||||
- name: rabbitmq-data
|
||||
mountPath: /var/lib/rabbitmq
|
||||
volumes:
|
||||
- name: rabbitmq-certs-volume
|
||||
secret:
|
||||
secretName: rabbitmq-certs
|
||||
- name: rabbitmq-config-volume
|
||||
configMap:
|
||||
name: rabbitmq-config
|
||||
- name: rabbitmq-advanced-config-volume
|
||||
configMap:
|
||||
name: rabbitmq-config
|
||||
- name: plugins-config-volume
|
||||
configMap:
|
||||
name: rabbit-plugins-conf
|
||||
- name: users-config-volume
|
||||
configMap:
|
||||
name: rabbitmq-users-definitions
|
||||
- name: rabbitmq-data
|
||||
emptyDir: {}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rabbit-plugins-conf
|
||||
data:
|
||||
enabled_plugins: |
|
||||
[rabbitmq_auth_mechanism_ssl, rabbitmq_management, rabbitmq_management_agent, rabbitmq_prometheus, rabbitmq_web_dispatch].
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: rabbitmq-secret
|
||||
type: Opaque
|
||||
stringData:
|
||||
rabbitmq-user: "coslight"
|
||||
rabbitmq-pass: "coslight@tj"
|
||||
erlang-cookie: "secret-erlang-cookie"
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: rabbitmq-service
|
||||
spec:
|
||||
type: NodePort # 在 Minikube 中使用 NodePort 方便外部访问
|
||||
selector:
|
||||
app: rabbitmq
|
||||
ports:
|
||||
- name: amqp-ssl
|
||||
protocol: TCP
|
||||
port: 5671
|
||||
targetPort: 5671
|
||||
nodePort: 30671
|
||||
- name: amqp
|
||||
protocol: TCP
|
||||
port: 5672
|
||||
targetPort: 5672
|
||||
nodePort: 30672
|
||||
- name: management-ssl
|
||||
protocol: TCP
|
||||
port: 15671
|
||||
targetPort: 15671
|
||||
nodePort: 31671
|
||||
- name: management
|
||||
protocol: TCP
|
||||
port: 15672
|
||||
targetPort: 15672
|
||||
nodePort: 31672
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rabbitmq-users-definitions
|
||||
data:
|
||||
definitions.json: |
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"name": "coslight",
|
||||
"password_hash": "Gl2XVEJwPwDZQF8ZhsYnvm83wMkdftY3/raxyntdZueyx/Uv",
|
||||
"hashing_algorithm": "rabbit_password_hashing_sha256",
|
||||
"tags": ["administrator"]
|
||||
},
|
||||
{
|
||||
"name": "web-client",
|
||||
"password_hash": "",
|
||||
"hashing_algorithm": "rabbit_password_hashing_sha256",
|
||||
"tags": ["management"]
|
||||
},
|
||||
{
|
||||
"name": "modelrt-client",
|
||||
"password_hash": "",
|
||||
"hashing_algorithm": "rabbit_password_hashing_sha256",
|
||||
"tags": ["management"]
|
||||
},
|
||||
{
|
||||
"name": "eventrt-client",
|
||||
"password_hash": "",
|
||||
"hashing_algorithm": "rabbit_password_hashing_sha256",
|
||||
"tags": ["management"]
|
||||
}
|
||||
],
|
||||
"vhosts": [ { "name": "/" } ],
|
||||
"permissions": [
|
||||
{
|
||||
"user": "coslight",
|
||||
"vhost": "/",
|
||||
"configure": ".*",
|
||||
"write": ".*",
|
||||
"read": ".*"
|
||||
},
|
||||
{
|
||||
"user": "web-client",
|
||||
"vhost": "/",
|
||||
"configure": "^$",
|
||||
"write": ".*",
|
||||
"read": ".*"
|
||||
},
|
||||
{
|
||||
"user": "modelrt-client",
|
||||
"vhost": "/",
|
||||
"configure": ".*",
|
||||
"write": ".*",
|
||||
"read": ".*"
|
||||
},
|
||||
{
|
||||
"user": "eventrt-client",
|
||||
"vhost": "/",
|
||||
"configure": ".*",
|
||||
"write": ".*",
|
||||
"read": ".*"
|
||||
}
|
||||
],
|
||||
"topic_permissions": [],
|
||||
"parameters": [],
|
||||
"global_parameters": [
|
||||
{
|
||||
"name": "cluster_name",
|
||||
"value": "evnetrt-rabbitmq-cluster"
|
||||
}
|
||||
],
|
||||
"policies": [],
|
||||
"queues": [],
|
||||
"exchanges": [],
|
||||
"bindings": []
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis/redis-stack-server:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-service
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
nodePort: 30001
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ func generateNormalData(baseValue, normalBase float64) []float64 {
|
|||
func main() {
|
||||
rootCtx := context.Background()
|
||||
|
||||
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "localhost", 5432, "postgres", "coslight", "develop_env")
|
||||
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "192.168.1.101", 5432, "postgres", "coslight", "demo")
|
||||
|
||||
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
|
||||
if err != nil {
|
||||
|
|
@ -164,6 +164,7 @@ func main() {
|
|||
}
|
||||
|
||||
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
|
||||
// log.Printf("key:%s\n datas:%v\n", key, datas)
|
||||
|
||||
allHigh := true
|
||||
for i := highStart; i < highEnd; i++ {
|
||||
|
|
|
|||
|
|
@ -129,9 +129,9 @@ func generateOutlierSegments(totalSize, minLength, maxLength, count int, distrib
|
|||
segments := make([]OutlierSegment, 0, count)
|
||||
usedPositions := make(map[int]bool)
|
||||
|
||||
for range count {
|
||||
for i := 0; i < count; i++ {
|
||||
// 尝试多次寻找合适的位置
|
||||
for range 10 {
|
||||
for attempt := 0; attempt < 10; attempt++ {
|
||||
length := rand.Intn(maxLength-minLength+1) + minLength
|
||||
start := rand.Intn(totalSize - length)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package util
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"modelRT/orm"
|
||||
)
|
||||
|
|
@ -62,7 +61,7 @@ func ProcessMeasurements(measurements []orm.Measurement) map[string]CalculationR
|
|||
device, _ := ioAddress["device"].(string)
|
||||
channel, _ := ioAddress["channel"].(string)
|
||||
|
||||
result := strings.ToLower(fmt.Sprintf("%s:%s:phasor:%s", station, device, channel))
|
||||
result := fmt.Sprintf("%s:%s:phasor:%s", station, device, channel)
|
||||
if measurement.EventPlan == nil {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
// Package diagram provide diagram data structure and operation
|
||||
package diagram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"modelRT/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// anchorValueOverview define struct of storage all anchor value keyed by component uuid
|
||||
var anchorValueOverview util.TypedMap[string, string]
|
||||
// anchorValueOverview define struct of storage all anchor value
|
||||
var anchorValueOverview sync.Map
|
||||
|
||||
// GetAnchorValue define func of get circuit diagram data by componentID
|
||||
func GetAnchorValue(componentUUID string) (string, error) {
|
||||
anchorValue, ok := anchorValueOverview.Load(componentUUID)
|
||||
value, ok := diagramsOverview.Load(componentUUID)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("can not find anchor value by componentUUID:%s", componentUUID)
|
||||
}
|
||||
anchorValue, ok := value.(string)
|
||||
if !ok {
|
||||
return "", errors.New("convert to string failed")
|
||||
}
|
||||
return anchorValue, nil
|
||||
}
|
||||
|
||||
|
|
@ -28,9 +31,11 @@ func UpdateAnchorValue(componentUUID string, anchorValue string) bool {
|
|||
// StoreAnchorValue define func of store anchor value with componentUUID and anchor name
|
||||
func StoreAnchorValue(componentUUID string, anchorValue string) {
|
||||
anchorValueOverview.Store(componentUUID, anchorValue)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteAnchorValue define func of delete anchor value with componentUUID
|
||||
func DeleteAnchorValue(componentUUID string) {
|
||||
anchorValueOverview.Delete(componentUUID)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,43 @@
|
|||
// Package diagram provide diagram data structure and operation
|
||||
package diagram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"modelRT/orm"
|
||||
"modelRT/util"
|
||||
)
|
||||
|
||||
// diagramsOverview define struct of storage all circuit diagram data keyed by component uuid
|
||||
var diagramsOverview util.TypedMap[string, *orm.Component]
|
||||
// diagramsOverview define struct of storage all circuit diagram data
|
||||
var diagramsOverview sync.Map
|
||||
|
||||
// GetComponentMap define func of get circuit diagram data by component uuid
|
||||
func GetComponentMap(componentUUID string) (*orm.Component, error) {
|
||||
componentInfo, ok := diagramsOverview.Load(componentUUID)
|
||||
value, ok := diagramsOverview.Load(componentUUID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can not find graph by global uuid:%s", componentUUID)
|
||||
}
|
||||
componentInfo, ok := value.(*orm.Component)
|
||||
if !ok {
|
||||
return nil, errors.New("convert to component map struct failed")
|
||||
}
|
||||
return componentInfo, nil
|
||||
}
|
||||
|
||||
// UpdateComponentMap define func of update circuit diagram data by component uuid and component info
|
||||
func UpdateComponentMap(componentUUID string, componentInfo *orm.Component) bool {
|
||||
_, result := diagramsOverview.Swap(componentUUID, componentInfo)
|
||||
func UpdateComponentMap(componentID int64, componentInfo *orm.Component) bool {
|
||||
_, result := diagramsOverview.Swap(componentID, componentInfo)
|
||||
return result
|
||||
}
|
||||
|
||||
// StoreComponentMap define func of store circuit diagram data with component uuid and component info
|
||||
func StoreComponentMap(componentUUID string, componentInfo *orm.Component) {
|
||||
diagramsOverview.Store(componentUUID, componentInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteComponentMap define func of delete circuit diagram data with component uuid
|
||||
func DeleteComponentMap(componentUUID string) {
|
||||
diagramsOverview.Delete(componentUUID)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package diagram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"modelRT/common"
|
||||
"modelRT/constants"
|
||||
)
|
||||
|
||||
func clientTokenFromContext(ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "", common.ErrGetClientToken
|
||||
}
|
||||
token, ok := ctx.Value(constants.CtxKeyClientToken).(string)
|
||||
if !ok || token == "" {
|
||||
return "", fmt.Errorf("%w: missing or invalid context value", common.ErrGetClientToken)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package diagram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"modelRT/common"
|
||||
"modelRT/constants"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClientTokenFromContext(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), constants.CtxKeyClientToken, "test-token")
|
||||
token, err := clientTokenFromContext(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-token", token)
|
||||
}
|
||||
|
||||
func TestClientTokenFromContextReturnsErrorWhenMissing(t *testing.T) {
|
||||
for _, ctx := range []context.Context{nil, context.Background()} {
|
||||
_, err := clientTokenFromContext(ctx)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, common.ErrGetClientToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisConstructorsReturnErrorInsteadOfPanickingWithoutToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := NewRedisZSet(ctx, "zset", 0, false)
|
||||
assert.ErrorIs(t, err, common.ErrGetClientToken)
|
||||
_, err = NewRedisSet(ctx, "set", 0, false)
|
||||
assert.ErrorIs(t, err, common.ErrGetClientToken)
|
||||
_, err = NewRedisHash(ctx, "hash", 0, false)
|
||||
assert.ErrorIs(t, err, common.ErrGetClientToken)
|
||||
}
|
||||
|
|
@ -65,7 +65,9 @@ func (g *Graph) AddEdge(from, to uuid.UUID) {
|
|||
|
||||
// 创建新的拓扑信息时,如果被链接的点已经存在于游离节点中
|
||||
// 则将其移除
|
||||
delete(g.FreeVertexs, toKey)
|
||||
if _, exist := g.FreeVertexs[toKey]; exist {
|
||||
delete(g.FreeVertexs, toKey)
|
||||
}
|
||||
}
|
||||
|
||||
// DelNode delete a node to the graph
|
||||
|
|
@ -110,6 +112,7 @@ func (g *Graph) DelEdge(from, to uuid.UUID) error {
|
|||
return fmt.Errorf("delete edge failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("fromKeys:", fromKeys)
|
||||
for _, fromUUID := range fromKeys {
|
||||
fromKey := fromUUID.String()
|
||||
var delIndex int
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// Package diagram provide diagram data structure and operation
|
||||
package diagram
|
||||
|
||||
import (
|
||||
|
|
@ -18,7 +17,7 @@ func TestHMSet(t *testing.T) {
|
|||
PoolSize: 50,
|
||||
DialTimeout: 10 * time.Second,
|
||||
})
|
||||
params := map[string]any{
|
||||
params := map[string]interface{}{
|
||||
"field1": "Hello1",
|
||||
"field2": "World1",
|
||||
"field3": 11,
|
||||
|
|
@ -30,4 +29,5 @@ func TestHMSet(t *testing.T) {
|
|||
fmt.Printf("err:%v\n", err)
|
||||
}
|
||||
fmt.Printf("res:%v\n", res)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package diagram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
var GlobalTree *MultiBranchTreeNode
|
||||
|
||||
// MultiBranchTreeNode represents a topological structure using an multi branch tree
|
||||
type MultiBranchTreeNode struct {
|
||||
ID uuid.UUID // 节点唯一标识
|
||||
Parent *MultiBranchTreeNode // 指向父节点的指针
|
||||
Children []*MultiBranchTreeNode // 指向所有子节点的指针切片
|
||||
}
|
||||
|
||||
func NewMultiBranchTree(id uuid.UUID) *MultiBranchTreeNode {
|
||||
return &MultiBranchTreeNode{
|
||||
ID: id,
|
||||
Children: make([]*MultiBranchTreeNode, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *MultiBranchTreeNode) AddChild(child *MultiBranchTreeNode) {
|
||||
child.Parent = n
|
||||
n.Children = append(n.Children, child)
|
||||
}
|
||||
|
||||
func (n *MultiBranchTreeNode) RemoveChild(childID uuid.UUID) bool {
|
||||
for i, child := range n.Children {
|
||||
if child.ID == childID {
|
||||
n.Children = append(n.Children[:i], n.Children[i+1:]...)
|
||||
child.Parent = nil
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *MultiBranchTreeNode) FindNodeByID(id uuid.UUID) *MultiBranchTreeNode {
|
||||
if n.ID == id {
|
||||
return n
|
||||
}
|
||||
|
||||
for _, child := range n.Children {
|
||||
if found := child.FindNodeByID(id); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *MultiBranchTreeNode) PrintTree(level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
|
||||
fmt.Printf("-ID: %s\n", n.ID)
|
||||
|
||||
for _, child := range n.Children {
|
||||
child.PrintTree(level + 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ package diagram
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
|
@ -14,46 +12,6 @@ type RedisClient struct {
|
|||
Client *redis.Client
|
||||
}
|
||||
|
||||
// QueryLatestMeasurementValue returns the score whose member contains the
|
||||
// greatest numeric timestamp. Measurement ZSets currently store timestamp in
|
||||
// member and measurement value in score.
|
||||
func (rc *RedisClient) QueryLatestMeasurementValue(ctx context.Context, key string) (float64, error) {
|
||||
if rc.Client == nil {
|
||||
return 0, fmt.Errorf("redis client is not initialized")
|
||||
}
|
||||
|
||||
members, err := rc.Client.ZRangeWithScores(ctx, key, 0, -1).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return latestMeasurementValue(members, key)
|
||||
}
|
||||
|
||||
func latestMeasurementValue(members []redis.Z, key string) (float64, error) {
|
||||
if len(members) == 0 {
|
||||
return 0, fmt.Errorf("real-time measurement value not found for key %q", key)
|
||||
}
|
||||
|
||||
var latestTimestamp int64
|
||||
var latestValue float64
|
||||
found := false
|
||||
for _, member := range members {
|
||||
timestamp, err := strconv.ParseInt(fmt.Sprint(member.Member), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !found || timestamp > latestTimestamp {
|
||||
latestTimestamp = timestamp
|
||||
latestValue = member.Score
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 0, fmt.Errorf("real-time measurement timestamps are invalid for key %q", key)
|
||||
}
|
||||
return latestValue, nil
|
||||
}
|
||||
|
||||
// NewRedisClient define func of new redis client instance
|
||||
func NewRedisClient() *RedisClient {
|
||||
return &RedisClient{
|
||||
|
|
@ -61,8 +19,8 @@ func NewRedisClient() *RedisClient {
|
|||
}
|
||||
}
|
||||
|
||||
// QueryByZRange define func to query real time data from redis zset
|
||||
func (rc *RedisClient) QueryByZRange(ctx context.Context, key string, size int64) ([]redis.Z, error) {
|
||||
// QueryByZRangeByLex define func to query real time data from redis zset
|
||||
func (rc *RedisClient) QueryByZRangeByLex(ctx context.Context, key string, size int64) ([]redis.Z, error) {
|
||||
client := rc.Client
|
||||
args := redis.ZRangeArgs{
|
||||
Key: key,
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
package diagram
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLatestMeasurementValueUsesMemberTimestamp(t *testing.T) {
|
||||
value, err := latestMeasurementValue([]redis.Z{
|
||||
{Member: "100", Score: 999},
|
||||
{Member: "300", Score: 12},
|
||||
{Member: "200", Score: 500},
|
||||
}, "measurement-key")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, float64(12), value)
|
||||
}
|
||||
|
||||
func TestLatestMeasurementValueRejectsMissingOrInvalidTimestamps(t *testing.T) {
|
||||
_, err := latestMeasurementValue(nil, "measurement-key")
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = latestMeasurementValue([]redis.Z{{Member: "invalid", Score: 1}}, "measurement-key")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
@ -18,21 +18,18 @@ type RedisHash struct {
|
|||
}
|
||||
|
||||
// NewRedisHash define func of new redis hash instance
|
||||
func NewRedisHash(ctx context.Context, hashKey string, lockLeaseTime uint64, needRefresh bool) (*RedisHash, error) {
|
||||
token, err := clientTokenFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewRedisHash(ctx context.Context, hashKey string, lockLeaseTime uint64, needRefresh bool) *RedisHash {
|
||||
token := ctx.Value("client_token").(string)
|
||||
return &RedisHash{
|
||||
ctx: ctx,
|
||||
hashKey: hashKey,
|
||||
rwLocker: locker.InitRWLocker(hashKey, token, lockLeaseTime, needRefresh),
|
||||
storageClient: GetRedisClientInstance(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetRedisHashByMap define func of set redis hash by map struct
|
||||
func (rh *RedisHash) SetRedisHashByMap(fields map[string]any) error {
|
||||
func (rh *RedisHash) SetRedisHashByMap(fields map[string]interface{}) error {
|
||||
err := rh.rwLocker.WLock(rh.ctx)
|
||||
if err != nil {
|
||||
logger.Error(rh.ctx, "lock wLock by hash_key failed", "hash_key", rh.hashKey, "error", err)
|
||||
|
|
@ -49,7 +46,7 @@ func (rh *RedisHash) SetRedisHashByMap(fields map[string]any) error {
|
|||
}
|
||||
|
||||
// SetRedisHashByKV define func of set redis hash by kv struct
|
||||
func (rh *RedisHash) SetRedisHashByKV(field string, value any) error {
|
||||
func (rh *RedisHash) SetRedisHashByKV(field string, value interface{}) error {
|
||||
err := rh.rwLocker.WLock(rh.ctx)
|
||||
if err != nil {
|
||||
logger.Error(rh.ctx, "lock wLock by hash_key failed", "hash_key", rh.hashKey, "error", err)
|
||||
|
|
|
|||
|
|
@ -16,15 +16,13 @@ var (
|
|||
)
|
||||
|
||||
// initClient define func of return successfully initialized redis client
|
||||
func initClient(rCfg config.RedisConfig, deployEnv string) *redis.Client {
|
||||
func initClient(rCfg config.RedisConfig) *redis.Client {
|
||||
client, err := util.NewRedisClient(
|
||||
rCfg.Addr,
|
||||
util.WithPassword(rCfg.Password, deployEnv),
|
||||
util.WithPassword(rCfg.Password),
|
||||
util.WithDB(rCfg.DB),
|
||||
util.WithPoolSize(rCfg.PoolSize),
|
||||
util.WithConnectTimeout(time.Duration(rCfg.DialTimeout)*time.Second),
|
||||
util.WithReadTimeout(time.Duration(rCfg.ReadTimeout)*time.Second),
|
||||
util.WithWriteTimeout(time.Duration(rCfg.WriteTimeout)*time.Second),
|
||||
util.WithTimeout(time.Duration(rCfg.Timeout)*time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -33,9 +31,9 @@ func initClient(rCfg config.RedisConfig, deployEnv string) *redis.Client {
|
|||
}
|
||||
|
||||
// InitRedisClientInstance define func of return instance of redis client
|
||||
func InitRedisClientInstance(rCfg config.RedisConfig, deployEnv string) *redis.Client {
|
||||
func InitRedisClientInstance(rCfg config.RedisConfig) *redis.Client {
|
||||
once.Do(func() {
|
||||
_globalStorageClient = initClient(rCfg, deployEnv)
|
||||
_globalStorageClient = initClient(rCfg)
|
||||
})
|
||||
return _globalStorageClient
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,18 +21,15 @@ type RedisSet struct {
|
|||
}
|
||||
|
||||
// NewRedisSet define func of new redis set instance
|
||||
func NewRedisSet(ctx context.Context, setKey string, lockLeaseTime uint64, needRefresh bool) (*RedisSet, error) {
|
||||
token, err := clientTokenFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewRedisSet(ctx context.Context, setKey string, lockLeaseTime uint64, needRefresh bool) *RedisSet {
|
||||
token := ctx.Value("client_token").(string)
|
||||
return &RedisSet{
|
||||
ctx: ctx,
|
||||
key: setKey,
|
||||
rwLocker: locker.InitRWLocker(setKey, token, lockLeaseTime, needRefresh),
|
||||
storageClient: GetRedisClientInstance(),
|
||||
logger: logger.GetLoggerInstance(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SADD define func of add redis set by members
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func (rs *RedisString) Get(stringKey string) (string, error) {
|
|||
}
|
||||
|
||||
// Set define func of set the value of key
|
||||
func (rs *RedisString) Set(stringKey string, value any) error {
|
||||
func (rs *RedisString) Set(stringKey string, value interface{}) error {
|
||||
err := rs.rwLocker.WLock(rs.ctx)
|
||||
if err != nil {
|
||||
logger.Error(rs.ctx, "lock wLock by stringKey failed", "string_key", stringKey, "error", err)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package diagram
|
|||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
"maps"
|
||||
|
||||
locker "modelRT/distributedlock"
|
||||
"modelRT/logger"
|
||||
|
|
@ -18,20 +20,17 @@ type RedisZSet struct {
|
|||
}
|
||||
|
||||
// NewRedisZSet define func of new redis zset instance
|
||||
func NewRedisZSet(ctx context.Context, key string, lockLeaseTime uint64, needRefresh bool) (*RedisZSet, error) {
|
||||
token, err := clientTokenFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewRedisZSet(ctx context.Context, key string, lockLeaseTime uint64, needRefresh bool) *RedisZSet {
|
||||
token := ctx.Value("client_token").(string)
|
||||
return &RedisZSet{
|
||||
ctx: ctx,
|
||||
rwLocker: locker.InitRWLocker(key, token, lockLeaseTime, needRefresh),
|
||||
storageClient: GetRedisClientInstance(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ZADD define func of add redis zset by members
|
||||
func (rs *RedisZSet) ZADD(setKey string, score float64, member any) error {
|
||||
func (rs *RedisZSet) ZADD(setKey string, score float64, member interface{}) error {
|
||||
err := rs.rwLocker.WLock(rs.ctx)
|
||||
if err != nil {
|
||||
logger.Error(rs.ctx, "lock wLock by setKey failed", "set_key", setKey, "error", err)
|
||||
|
|
@ -47,26 +46,6 @@ func (rs *RedisZSet) ZADD(setKey string, score float64, member any) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ZREPLACE atomically removes all existing members and adds one new member.
|
||||
func (rs *RedisZSet) ZREPLACE(setKey string, score float64, member any) error {
|
||||
if err := rs.rwLocker.WLock(rs.ctx); err != nil {
|
||||
logger.Error(rs.ctx, "lock wLock by setKey failed", "set_key", setKey, "error", err)
|
||||
return err
|
||||
}
|
||||
defer rs.rwLocker.UnWLock(rs.ctx)
|
||||
|
||||
_, err := rs.storageClient.TxPipelined(rs.ctx, func(pipe redis.Pipeliner) error {
|
||||
pipe.Del(rs.ctx, setKey)
|
||||
pipe.ZAdd(rs.ctx, setKey, redis.Z{Score: score, Member: member})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(rs.ctx, "replace zset member failed", "set_key", setKey, "member", member, "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ZRANGE define func of returns the specified range of elements in the sorted set stored by key
|
||||
func (rs *RedisZSet) ZRANGE(setKey string, start, stop int64) ([]string, error) {
|
||||
var results []string
|
||||
|
|
@ -91,3 +70,55 @@ func (rs *RedisZSet) ZRANGE(setKey string, start, stop int64) ([]string, error)
|
|||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
type Comparer[T any] interface {
|
||||
Compare(T) int
|
||||
}
|
||||
|
||||
type ComparableComparer[T any] interface {
|
||||
Compare(T) int
|
||||
comparable // 直接嵌入 comparable 约束
|
||||
}
|
||||
|
||||
type methodNode[E Comparer[E]] struct {
|
||||
value E
|
||||
left *methodNode[E]
|
||||
right *methodNode[E]
|
||||
}
|
||||
|
||||
type MethodTree[E Comparer[E]] struct {
|
||||
root *methodNode[E]
|
||||
}
|
||||
|
||||
type OrderedSet[E interface {
|
||||
comparable
|
||||
Comparer[E]
|
||||
}] struct {
|
||||
tree MethodTree[E]
|
||||
elements map[E]bool
|
||||
}
|
||||
|
||||
type ComparableOrderedSet[E ComparableComparer[E]] struct {
|
||||
tree MethodTree[E]
|
||||
elements map[E]bool
|
||||
}
|
||||
|
||||
type Set[E any] interface {
|
||||
Insert(E)
|
||||
Delete(E)
|
||||
Has(E) bool
|
||||
All() iter.Seq[E]
|
||||
}
|
||||
|
||||
func InsertAll[E any](set Set[E], seq iter.Seq[E]) {
|
||||
for v := range seq {
|
||||
set.Insert(v)
|
||||
}
|
||||
}
|
||||
|
||||
type HashSet[E comparable] map[E]bool
|
||||
|
||||
func (s HashSet[E]) Insert(v E) { s[v] = true }
|
||||
func (s HashSet[E]) Delete(v E) { delete(s, v) }
|
||||
func (s HashSet[E]) Has(v E) bool { return s[v] }
|
||||
func (s HashSet[E]) All() iter.Seq[E] { return maps.Keys(s) }
|
||||
|
|
|
|||
|
|
@ -1,28 +1,32 @@
|
|||
// Package diagram provide diagram data structure and operation
|
||||
package diagram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"modelRT/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// graphOverview define struct of storage all circuit diagram topologic data keyed by pageID
|
||||
var graphOverview util.TypedMap[int64, *Graph]
|
||||
// graphOverview define struct of storage all circuit diagram topologic data
|
||||
var graphOverview sync.Map
|
||||
|
||||
// PrintGrapMap define func of print circuit diagram topologic info data
|
||||
func PrintGrapMap() {
|
||||
for pageID, graph := range graphOverview.All() {
|
||||
fmt.Println(pageID, graph)
|
||||
}
|
||||
graphOverview.Range(func(key, value interface{}) bool {
|
||||
fmt.Println(key, value)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetGraphMap define func of get circuit diagram topologic data by pageID
|
||||
func GetGraphMap(pageID int64) (*Graph, error) {
|
||||
graph, ok := graphOverview.Load(pageID)
|
||||
value, ok := graphOverview.Load(pageID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can not find graph by pageID:%d", pageID)
|
||||
}
|
||||
graph, ok := value.(*Graph)
|
||||
if !ok {
|
||||
return nil, errors.New("convert to graph struct failed")
|
||||
}
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
|
|
@ -35,9 +39,11 @@ func UpdateGrapMap(pageID int64, graphInfo *Graph) bool {
|
|||
// StoreGraphMap define func of store circuit diagram topologic data with pageID and topologic info
|
||||
func StoreGraphMap(pageID int64, graphInfo *Graph) {
|
||||
graphOverview.Store(pageID, graphInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteGraphMap define func of delete circuit diagram topologic data with pageID
|
||||
func DeleteGraphMap(pageID int64) {
|
||||
graphOverview.Delete(pageID)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue