From 4bd1afa35600d336a96f9b5e70131dc0de854eae Mon Sep 17 00:00:00 2001 From: Terence Lim Date: Thu, 2 Mar 2023 17:49:12 +0800 Subject: [PATCH] Update plugin --- api/schema.yaml | 17 +++- .../management/ManagementClientInterface.go | 2 +- .../treatment/TreatmentClientInterface.go | 2 +- common/api/schema/schema.go | 89 +++++++++++-------- .../services/configuration_service.go | 23 ++++- .../services/configuration_service_test.go | 11 ++- .../services/message_queue_service.go | 2 +- plugins/turing/manager/experiment_manager.go | 31 +++++-- plugins/turing/runner/experiment_runner.go | 4 +- treatment-service/config/config.go | 6 +- 10 files changed, 127 insertions(+), 60 deletions(-) diff --git a/api/schema.yaml b/api/schema.yaml index a6e04998..4d29a867 100644 --- a/api/schema.yaml +++ b/api/schema.yaml @@ -9,13 +9,26 @@ components: topic_name: type: string description: Topic name of the PubSub subscription + MessageQueueKind: + description: Kind of message queue + type: string + enum: + - noop + - pubsub + MessageQueueConfig: + type: object + properties: + kind: + $ref: '#/components/schemas/MessageQueueKind' + pub_sub: + $ref: '#/components/schemas/PubSub' SegmenterConfig: type: object TreatmentServiceConfig: type: object properties: - pub_sub: - $ref: '#/components/schemas/PubSub' + message_queue_config: + $ref: '#/components/schemas/MessageQueueConfig' segmenter_config: $ref: '#/components/schemas/SegmenterConfig' SelectedTreatmentData: diff --git a/clients/testutils/mocks/management/ManagementClientInterface.go b/clients/testutils/mocks/management/ManagementClientInterface.go index 7a13f416..3f3b4962 100644 --- a/clients/testutils/mocks/management/ManagementClientInterface.go +++ b/clients/testutils/mocks/management/ManagementClientInterface.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/clients/testutils/mocks/treatment/TreatmentClientInterface.go b/clients/testutils/mocks/treatment/TreatmentClientInterface.go index 2a93fbf5..af6dd1a2 100644 --- a/clients/testutils/mocks/treatment/TreatmentClientInterface.go +++ b/clients/testutils/mocks/treatment/TreatmentClientInterface.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package mocks diff --git a/common/api/schema/schema.go b/common/api/schema/schema.go index 73b08b9b..2af6877d 100644 --- a/common/api/schema/schema.go +++ b/common/api/schema/schema.go @@ -71,6 +71,13 @@ const ( ExperimentTypeSwitchback ExperimentType = "Switchback" ) +// Defines values for MessageQueueKind. +const ( + MessageQueueKindNoop MessageQueueKind = "noop" + + MessageQueueKindPubsub MessageQueueKind = "pubsub" +) + // Defines values for SegmentField. const ( SegmentFieldId SegmentField = "id" @@ -203,6 +210,17 @@ type ExperimentTreatment struct { // ExperimentType defines model for ExperimentType. type ExperimentType string +// MessageQueueConfig defines model for MessageQueueConfig. +type MessageQueueConfig struct { + + // Kind of message queue + Kind *MessageQueueKind `json:"kind,omitempty"` + PubSub *PubSub `json:"pub_sub,omitempty"` +} + +// Kind of message queue +type MessageQueueKind string + // Paging defines model for Paging. type Paging struct { @@ -416,8 +434,8 @@ type TreatmentSchema struct { // TreatmentServiceConfig defines model for TreatmentServiceConfig. type TreatmentServiceConfig struct { - PubSub *PubSub `json:"pub_sub,omitempty"` - SegmenterConfig *SegmenterConfig `json:"segmenter_config,omitempty"` + MessageQueueConfig *MessageQueueConfig `json:"message_queue_config,omitempty"` + SegmenterConfig *SegmenterConfig `json:"segmenter_config,omitempty"` } // Getter for additional properties for ProjectSegmenters_Variables. Returns the specified @@ -529,39 +547,40 @@ func (a SegmenterOptions) MarshalJSON() ([]byte, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/management-service/services/configuration_service.go b/management-service/services/configuration_service.go index d0a180f9..3cd0f859 100644 --- a/management-service/services/configuration_service.go +++ b/management-service/services/configuration_service.go @@ -16,15 +16,30 @@ type configurationService struct { func NewConfigurationService(cfg *config.Config) ConfigurationService { var segmenterConfig schema.SegmenterConfig = cfg.SegmenterConfig - return &configurationService{ + var messageQueueKind schema.MessageQueueKind + switch cfg.MessageQueueConfig.Kind { + case "pubsub": + messageQueueKind = schema.MessageQueueKindPubsub + case "": + messageQueueKind = schema.MessageQueueKindNoop + } + + configurationSvc := &configurationService{ treatmentServiceConfig: schema.TreatmentServiceConfig{ - PubSub: &schema.PubSub{ - Project: &cfg.MessageQueueConfig.PubSubConfig.Project, - TopicName: &cfg.MessageQueueConfig.PubSubConfig.TopicName, + MessageQueueConfig: &schema.MessageQueueConfig{ + Kind: &messageQueueKind, }, SegmenterConfig: &segmenterConfig, }, } + if cfg.MessageQueueConfig.Kind == "pubsub" { + configurationSvc.treatmentServiceConfig.MessageQueueConfig.PubSub = &schema.PubSub{ + Project: &cfg.MessageQueueConfig.PubSubConfig.Project, + TopicName: &cfg.MessageQueueConfig.PubSubConfig.TopicName, + } + } + + return configurationSvc } func (svc configurationService) GetTreatmentServiceConfig() schema.TreatmentServiceConfig { diff --git a/management-service/services/configuration_service_test.go b/management-service/services/configuration_service_test.go index 8e5c253c..8106a178 100644 --- a/management-service/services/configuration_service_test.go +++ b/management-service/services/configuration_service_test.go @@ -19,6 +19,7 @@ func (s *ConfigurationServiceTestSuite) SetupSuite() { cfg := config.Config{ MessageQueueConfig: &config.MessageQueueConfig{ + Kind: "pubsub", PubSubConfig: &config.PubSubConfig{ Project: "dev", TopicName: "xp-update", @@ -41,13 +42,17 @@ func TestConfigurationService(t *testing.T) { } func (s *ConfigurationServiceTestSuite) TestGetTreatmentServicePluginConfig() { + messageQueueKind := schema.MessageQueueKindPubsub pubSubConfigProject := "dev" pubSubConfigTopicName := "xp-update" expectedConfiguration := schema.TreatmentServiceConfig{ - PubSub: &schema.PubSub{ - Project: &pubSubConfigProject, - TopicName: &pubSubConfigTopicName, + MessageQueueConfig: &schema.MessageQueueConfig{ + Kind: &messageQueueKind, + PubSub: &schema.PubSub{ + Project: &pubSubConfigProject, + TopicName: &pubSubConfigTopicName, + }, }, SegmenterConfig: &schema.SegmenterConfig{ "s2_ids": map[string]interface{}{ diff --git a/management-service/services/message_queue_service.go b/management-service/services/message_queue_service.go index 437d8ccd..db4ee230 100644 --- a/management-service/services/message_queue_service.go +++ b/management-service/services/message_queue_service.go @@ -23,7 +23,7 @@ func NewMessageQueueService(mqConfig *config.MessageQueueConfig) (MessageQueueSe case config.PubSubMQ: mq, err = NewPubSubPublisherService(mqConfig.PubSubConfig) default: - return nil, fmt.Errorf("invalid message queue config (%s) was provided", mqConfig.Kind) + return nil, fmt.Errorf("invalid message queue kind (%s) was provided", mqConfig.Kind) } if err != nil { return nil, err diff --git a/plugins/turing/manager/experiment_manager.go b/plugins/turing/manager/experiment_manager.go index f9f9cbde..096e5750 100644 --- a/plugins/turing/manager/experiment_manager.go +++ b/plugins/turing/manager/experiment_manager.go @@ -142,7 +142,7 @@ func (em *experimentManager) MakeTreatmentServicePluginConfig( treatmentServiceConfig *schema.TreatmentServiceConfig, projectID int, ) (*config.Config, error) { - return &config.Config{ + pluginConfig := &config.Config{ Port: em.TreatmentServicePluginConfig.Port, ProjectIds: []string{strconv.Itoa(projectID)}, AssignedTreatmentLogger: em.TreatmentServicePluginConfig.AssignedTreatmentLogger, @@ -153,13 +153,28 @@ func (em *experimentManager) MakeTreatmentServicePluginConfig( SwaggerConfig: em.TreatmentServicePluginConfig.SwaggerConfig, NewRelicConfig: em.TreatmentServicePluginConfig.NewRelicConfig, SentryConfig: em.TreatmentServicePluginConfig.SentryConfig, - PubSub: config.PubSub{ - Project: *treatmentServiceConfig.PubSub.Project, - TopicName: *treatmentServiceConfig.PubSub.TopicName, - PubSubTimeoutSeconds: em.TreatmentServicePluginConfig.PubSubTimeoutSeconds, - }, - SegmenterConfig: *treatmentServiceConfig.SegmenterConfig, - }, nil + SegmenterConfig: *treatmentServiceConfig.SegmenterConfig, + } + messageQueueKind := *treatmentServiceConfig.MessageQueueConfig.Kind + switch messageQueueKind { + case schema.MessageQueueKindPubsub: + pluginConfig.MessageQueueConfig = config.MessageQueueConfig{ + Kind: "pubsub", + PubSubConfig: config.PubSub{ + Project: *treatmentServiceConfig.MessageQueueConfig.PubSub.Project, + TopicName: *treatmentServiceConfig.MessageQueueConfig.PubSub.TopicName, + PubSubTimeoutSeconds: em.TreatmentServicePluginConfig.PubSubTimeoutSeconds, + }, + } + case schema.MessageQueueKindNoop: + pluginConfig.MessageQueueConfig = config.MessageQueueConfig{ + Kind: "", + } + default: + return nil, fmt.Errorf("invalid message queue kind (%s) was provided", messageQueueKind) + } + + return pluginConfig, nil } func NewExperimentManager(configData json.RawMessage) (manager.CustomExperimentManager, error) { diff --git a/plugins/turing/runner/experiment_runner.go b/plugins/turing/runner/experiment_runner.go index 38205a0c..6814d16a 100644 --- a/plugins/turing/runner/experiment_runner.go +++ b/plugins/turing/runner/experiment_runner.go @@ -249,9 +249,9 @@ func (er *experimentRunner) startBackgroundServices( errChannel chan error, ) { backgroundSvcCtx := context.Background() - if er.appContext.ExperimentSubscriber != nil { + if er.appContext.MessageQueueService != nil { go func() { - err := er.appContext.ExperimentSubscriber.SubscribeToManagementService(backgroundSvcCtx) + err := er.appContext.MessageQueueService.SubscribeToManagementService(backgroundSvcCtx) if err != nil { errChannel <- err } diff --git a/treatment-service/config/config.go b/treatment-service/config/config.go index 6f8eafe6..c24d061f 100644 --- a/treatment-service/config/config.go +++ b/treatment-service/config/config.go @@ -107,9 +107,9 @@ type MessageQueueConfig struct { } type PubSub struct { - Project string `json:"project" default:"dev" validate:"required"` - TopicName string `json:"topic_name" default:"xp-update" validate:"required"` - PubSubTimeoutSeconds int `json:"pub_sub_timeout_seconds" default:"30" validate:"required"` + Project string `json:"project" default:"dev"` + TopicName string `json:"topic_name" default:"xp-update"` + PubSubTimeoutSeconds int `json:"pub_sub_timeout_seconds" default:"30"` } type ManagementServiceConfig struct {