From a0a5ab818f3d51819024be2a386c94d9acccdcbf Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Wed, 12 Nov 2025 00:31:11 +0530 Subject: [PATCH 1/6] Modules: Concurrency issue with module context fix --- exchange/exchange_test.go | 2 +- hooks/hookexecution/context.go | 25 +- hooks/hookexecution/executor.go | 2 +- hooks/hookexecution/executor_test.go | 314 +++++++++++------- hooks/hookexecution/mocks_test.go | 24 +- hooks/hookstage/invocation.go | 71 +++- .../devicedetection/evidence_extractor.go | 8 +- .../evidence_extractor_test.go | 108 +++--- .../hook_auction_entrypoint.go | 8 +- .../fiftyonedegrees/devicedetection/module.go | 2 +- .../devicedetection/module_test.go | 21 +- .../ortb2blocking/hook_bidderrequest.go | 5 +- .../ortb2blocking/hook_raw_bidder_response.go | 4 +- modules/prebid/ortb2blocking/module_test.go | 138 +++++--- modules/scope3/rtd/module.go | 43 ++- modules/scope3/rtd/module_test.go | 20 +- 16 files changed, 548 insertions(+), 247 deletions(-) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index cb214e89941..6a03335ca7d 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -5996,7 +5996,7 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mct }, hookstage.MutationUpdate, "bidRequest", "site.domain", ) - mctx.ModuleContext = map[string]interface{}{"some-ctx": "some-ctx"} + mctx.ModuleContext.Set("some-ctx", "some-ctx") return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: c, ModuleContext: mctx.ModuleContext}, nil } diff --git a/hooks/hookexecution/context.go b/hooks/hookexecution/context.go index 128008001cb..05d8ac4fe5c 100644 --- a/hooks/hookexecution/context.go +++ b/hooks/hookexecution/context.go @@ -43,24 +43,27 @@ func (ctx executionContext) getModuleContext(moduleName string) hookstage.Module // moduleContexts preserves data the module wants to pass to itself from earlier stages to later stages. type moduleContexts struct { sync.RWMutex - ctxs map[string]hookstage.ModuleContext // format: {"module_name": hookstage.ModuleContext} + ctxs map[string]*hookstage.ModuleContext // format: {"module_name": hookstage.ModuleContext} } -func (mc *moduleContexts) put(moduleName string, mCtx hookstage.ModuleContext) { +func (mc *moduleContexts) put(moduleName string, mCtx *hookstage.ModuleContext) { + if mc == nil { + return + } mc.Lock() defer mc.Unlock() - newCtx := mCtx - if existingCtx, ok := mc.ctxs[moduleName]; ok && existingCtx != nil { - for k, v := range mCtx { - existingCtx[k] = v - } - newCtx = existingCtx + existingCtx, ok := mc.ctxs[moduleName] + if !ok { + mc.ctxs[moduleName] = mCtx + return } - mc.ctxs[moduleName] = newCtx + + // Add new data to existing context + existingCtx.SetAll(mCtx.GetAll()) } -func (mc *moduleContexts) get(moduleName string) (hookstage.ModuleContext, bool) { +func (mc *moduleContexts) get(moduleName string) (*hookstage.ModuleContext, bool) { mc.RLock() defer mc.RUnlock() mCtx, ok := mc.ctxs[moduleName] @@ -72,4 +75,4 @@ type stageModuleContext struct { groupCtx []groupModuleContext } -type groupModuleContext map[string]hookstage.ModuleContext +type groupModuleContext map[string]*hookstage.ModuleContext diff --git a/hooks/hookexecution/executor.go b/hooks/hookexecution/executor.go index 0e5b28fff8e..eeab3af0b13 100644 --- a/hooks/hookexecution/executor.go +++ b/hooks/hookexecution/executor.go @@ -68,7 +68,7 @@ func NewHookExecutor(builder hooks.ExecutionPlanBuilder, endpoint string, me met endpoint: endpoint, planBuilder: builder, stageOutcomes: []StageOutcome{}, - moduleContexts: &moduleContexts{ctxs: make(map[string]hookstage.ModuleContext)}, + moduleContexts: &moduleContexts{ctxs: make(map[string]*hookstage.ModuleContext)}, metricEngine: me, } } diff --git a/hooks/hookexecution/executor_test.go b/hooks/hookexecution/executor_test.go index 4ce9933c834..bc81249ec4e 100644 --- a/hooks/hookexecution/executor_test.go +++ b/hooks/hookexecution/executor_test.go @@ -61,7 +61,7 @@ func TestExecuteEntrypointStage(t *testing.T) { const body string = `{"name": "John", "last_name": "Doe"}` const urlString string = "https://prebid.com/openrtb2/auction" - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} testCases := []struct { description string @@ -84,7 +84,7 @@ func TestExecuteEntrypointStage(t *testing.T) { expectedHeader: http.Header{}, expectedQuery: url.Values{}, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -299,9 +299,18 @@ func TestExecuteEntrypointStage(t *testing.T) { expectedHeader: http.Header{}, expectedQuery: url.Values{}, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"entrypoint-ctx-1": "some-ctx-1", "entrypoint-ctx-3": "some-ctx-3"}, - "module-2": {"entrypoint-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -418,7 +427,7 @@ func TestExecuteRawAuctionStage(t *testing.T) { const bodyUpdated string = `{"last_name": "Doe", "foo": "bar"}` const urlString string = "https://prebid.com/openrtb2/auction" - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} testCases := []struct { description string @@ -437,7 +446,7 @@ func TestExecuteRawAuctionStage(t *testing.T) { givenPlanBuilder: hooks.EmptyPlanBuilder{}, expectedBody: body, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -613,9 +622,18 @@ func TestExecuteRawAuctionStage(t *testing.T) { givenPlanBuilder: TestWithModuleContextsPlanBuilder{}, expectedBody: body, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"raw-auction-ctx-1": "some-ctx-1", "raw-auction-ctx-3": "some-ctx-3"}, - "module-2": {"raw-auction-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("raw-auction-ctx-1", "some-ctx-1") + mc.Set("raw-auction-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("raw-auction-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -691,7 +709,7 @@ func TestExecuteRawAuctionStage(t *testing.T) { } func TestExecuteProcessedAuctionStage(t *testing.T) { - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} req := openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}} reqUpdated := openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id", Yob: 2000, Consent: "true"}} @@ -710,7 +728,7 @@ func TestExecuteProcessedAuctionStage(t *testing.T) { givenRequest: openrtb_ext.RequestWrapper{BidRequest: &req}, expectedRequest: req, expectedErr: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -831,9 +849,18 @@ func TestExecuteProcessedAuctionStage(t *testing.T) { givenRequest: openrtb_ext.RequestWrapper{BidRequest: &req}, expectedRequest: req, expectedErr: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"processed-auction-ctx-1": "some-ctx-1", "processed-auction-ctx-3": "some-ctx-3"}, - "module-2": {"processed-auction-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("processed-auction-ctx-1", "some-ctx-1") + mc.Set("processed-auction-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("processed-auction-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -910,7 +937,7 @@ func TestExecuteProcessedAuctionStage(t *testing.T) { func TestExecuteBidderRequestStage(t *testing.T) { bidderName := "the-bidder" - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} expectedBidderRequest := &openrtb2.BidRequest{ID: "some-id", User: &openrtb2.User{ID: "user-id"}} expectedUpdatedBidderRequest := &openrtb2.BidRequest{ @@ -938,7 +965,7 @@ func TestExecuteBidderRequestStage(t *testing.T) { givenPlanBuilder: hooks.EmptyPlanBuilder{}, expectedBidderRequest: expectedBidderRequest, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -1106,9 +1133,17 @@ func TestExecuteBidderRequestStage(t *testing.T) { givenPlanBuilder: TestWithModuleContextsPlanBuilder{}, expectedBidderRequest: expectedBidderRequest, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"bidder-request-ctx-1": "some-ctx-1"}, - "module-2": {"bidder-request-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("bidder-request-ctx-1", "some-ctx-1") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("bidder-request-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -1218,7 +1253,7 @@ func buildDefaultActivityConfig(componentName string, allow bool) config.Activit } func TestExecuteRawBidderResponseStage(t *testing.T) { - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} resp := adapters.BidderResponse{Bids: []*adapters.TypedBid{{DealPriority: 1}}} expResp := adapters.BidderResponse{Bids: []*adapters.TypedBid{{DealPriority: 10}}} vEntity := entity("the-bidder") @@ -1238,7 +1273,7 @@ func TestExecuteRawBidderResponseStage(t *testing.T) { givenBidderResponse: resp, expectedBidderResponse: resp, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -1357,9 +1392,18 @@ func TestExecuteRawBidderResponseStage(t *testing.T) { givenBidderResponse: resp, expectedBidderResponse: expResp, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"raw-bidder-response-ctx-1": "some-ctx-1", "raw-bidder-response-ctx-3": "some-ctx-3"}, - "module-2": {"raw-bidder-response-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("raw-bidder-response-ctx-1", "some-ctx-1") + mc.Set("raw-bidder-response-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("raw-bidder-response-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -1435,7 +1479,7 @@ func TestExecuteRawBidderResponseStage(t *testing.T) { } func TestExecuteAllProcessedBidResponsesStage(t *testing.T) { - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} expectedAllProcBidResponses := map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{ "some-bidder": {Bids: []*entities.PbsOrtbBid{{DealPriority: 1}}}, @@ -1461,7 +1505,7 @@ func TestExecuteAllProcessedBidResponsesStage(t *testing.T) { givenPlanBuilder: hooks.EmptyPlanBuilder{}, expectedBiddersResponse: expectedAllProcBidResponses, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -1642,9 +1686,17 @@ func TestExecuteAllProcessedBidResponsesStage(t *testing.T) { givenPlanBuilder: TestWithModuleContextsPlanBuilder{}, expectedBiddersResponse: expectedAllProcBidResponses, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"all-processed-bid-responses-ctx-1": "some-ctx-1"}, - "module-2": {"all-processed-bid-responses-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("all-processed-bid-responses-ctx-1", "some-ctx-1") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("all-processed-bid-responses-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -1709,7 +1761,7 @@ func TestExecuteAllProcessedBidResponsesStage(t *testing.T) { } func TestExecuteAuctionResponseStage(t *testing.T) { - foobarModuleCtx := &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}} + foobarModuleCtx := &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}} resp := &openrtb2.BidResponse{CustomData: "some-custom-data"} expResp := &openrtb2.BidResponse{CustomData: "new-custom-data"} @@ -1728,7 +1780,7 @@ func TestExecuteAuctionResponseStage(t *testing.T) { givenResponse: resp, expectedResponse: resp, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -1877,9 +1929,18 @@ func TestExecuteAuctionResponseStage(t *testing.T) { givenResponse: resp, expectedResponse: resp, expectedReject: nil, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"auction-response-ctx-1": "some-ctx-1", "auction-response-ctx-3": "some-ctx-3"}, - "module-2": {"auction-response-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("auction-response-ctx-1", "some-ctx-1") + mc.Set("auction-response-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("auction-response-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -2000,7 +2061,7 @@ func TestExecuteExitpointStage(t *testing.T) { expectedResponseHeaders: http.Header{ "Content-Type": []string{"application/json"}, }, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{}}, expectedStageOutcomes: []StageOutcome{}, }, { @@ -2027,7 +2088,7 @@ func TestExecuteExitpointStage(t *testing.T) { expectedResponseHeaders: http.Header{ "Content-Type": []string{"application/xml"}, }, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}}, expectedStageOutcomes: []StageOutcome{ { Entity: "exitpoint", @@ -2068,7 +2129,7 @@ func TestExecuteExitpointStage(t *testing.T) { }, expectedResponse: ``, expectedResponseHeaders: http.Header{"Content-Type": []string{"application/xml"}}, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}}, expectedStageOutcomes: []StageOutcome{ { Entity: entityExitpoint, @@ -2144,7 +2205,7 @@ func TestExecuteExitpointStage(t *testing.T) { }, expectedResponse: ``, expectedResponseHeaders: http.Header{"Content-Type": []string{"application/xml"}}, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{"foobar": nil}}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{"foobar": nil}}, expectedStageOutcomes: []StageOutcome{ { Entity: entityExitpoint, @@ -2202,9 +2263,18 @@ func TestExecuteExitpointStage(t *testing.T) { }, expectedResponse: &openrtb2.BidResponse{ID: "test-id"}, expectedResponseHeaders: http.Header{"Content-Type": []string{"application/json"}}, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": {"exitpoint-ctx-1": "some-ctx-1", "exitpoint-ctx-3": "some-ctx-3"}, - "module-2": {"exitpoint-ctx-2": "some-ctx-2"}, + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("exitpoint-ctx-1", "some-ctx-1") + mc.Set("exitpoint-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("exitpoint-ctx-2", "some-ctx-2") + return mc + }(), }}, expectedStageOutcomes: []StageOutcome{ { @@ -2277,7 +2347,7 @@ func TestExecuteExitpointStage(t *testing.T) { expectedResponseHeaders: http.Header{ "Content-Type": []string{"application/json"}, }, - expectedModuleContexts: &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ + expectedModuleContexts: &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ "module-1": nil, "module-2": nil, }}, @@ -2345,12 +2415,18 @@ func TestInterStageContextCommunication(t *testing.T) { assert.Nil(t, reject, "Unexpected reject from entrypoint stage.") assert.Equal( t, - &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": { - "entrypoint-ctx-1": "some-ctx-1", - "entrypoint-ctx-3": "some-ctx-3", - }, - "module-2": {"entrypoint-ctx-2": "some-ctx-2"}, + &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + return mc + }(), }}, exec.moduleContexts, "Wrong module contexts after executing entrypoint hook.", @@ -2359,83 +2435,99 @@ func TestInterStageContextCommunication(t *testing.T) { // test that context added at the raw-auction stage merged with existing module contexts _, reject = exec.ExecuteRawAuctionStage(body) assert.Nil(t, reject, "Unexpected reject from raw-auction stage.") - assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": { - "entrypoint-ctx-1": "some-ctx-1", - "entrypoint-ctx-3": "some-ctx-3", - "raw-auction-ctx-1": "some-ctx-1", - "raw-auction-ctx-3": "some-ctx-3", - }, - "module-2": { - "entrypoint-ctx-2": "some-ctx-2", - "raw-auction-ctx-2": "some-ctx-2", - }, + assert.Equal(t, &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + mc.Set("raw-auction-ctx-1", "some-ctx-1") + mc.Set("raw-auction-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + mc.Set("raw-auction-ctx-2", "some-ctx-2") + return mc + }(), }}, exec.moduleContexts, "Wrong module contexts after executing raw-auction hook.") // test that context added at the processed-auction stage merged with existing module contexts err = exec.ExecuteProcessedAuctionStage(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}) assert.Nil(t, err, "Unexpected reject from processed-auction stage.") - assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": { - "entrypoint-ctx-1": "some-ctx-1", - "entrypoint-ctx-3": "some-ctx-3", - "raw-auction-ctx-1": "some-ctx-1", - "raw-auction-ctx-3": "some-ctx-3", - "processed-auction-ctx-1": "some-ctx-1", - "processed-auction-ctx-3": "some-ctx-3", - }, - "module-2": { - "entrypoint-ctx-2": "some-ctx-2", - "raw-auction-ctx-2": "some-ctx-2", - "processed-auction-ctx-2": "some-ctx-2", - }, + assert.Equal(t, &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + mc.Set("raw-auction-ctx-1", "some-ctx-1") + mc.Set("raw-auction-ctx-3", "some-ctx-3") + mc.Set("processed-auction-ctx-1", "some-ctx-1") + mc.Set("processed-auction-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + mc.Set("raw-auction-ctx-2", "some-ctx-2") + mc.Set("processed-auction-ctx-2", "some-ctx-2") + return mc + }(), }}, exec.moduleContexts, "Wrong module contexts after executing processed-auction hook.") // test that context added at the raw bidder response stage merged with existing module contexts reject = exec.ExecuteRawBidderResponseStage(&adapters.BidderResponse{}, "some-bidder") assert.Nil(t, reject, "Unexpected reject from raw-bidder-response stage.") - assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": { - "entrypoint-ctx-1": "some-ctx-1", - "entrypoint-ctx-3": "some-ctx-3", - "raw-auction-ctx-1": "some-ctx-1", - "raw-auction-ctx-3": "some-ctx-3", - "processed-auction-ctx-1": "some-ctx-1", - "processed-auction-ctx-3": "some-ctx-3", - "raw-bidder-response-ctx-1": "some-ctx-1", - "raw-bidder-response-ctx-3": "some-ctx-3", - }, - "module-2": { - "entrypoint-ctx-2": "some-ctx-2", - "raw-auction-ctx-2": "some-ctx-2", - "processed-auction-ctx-2": "some-ctx-2", - "raw-bidder-response-ctx-2": "some-ctx-2", - }, + assert.Equal(t, &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + mc.Set("raw-auction-ctx-1", "some-ctx-1") + mc.Set("raw-auction-ctx-3", "some-ctx-3") + mc.Set("processed-auction-ctx-1", "some-ctx-1") + mc.Set("processed-auction-ctx-3", "some-ctx-3") + mc.Set("raw-bidder-response-ctx-1", "some-ctx-1") + mc.Set("raw-bidder-response-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + mc.Set("raw-auction-ctx-2", "some-ctx-2") + mc.Set("processed-auction-ctx-2", "some-ctx-2") + mc.Set("raw-bidder-response-ctx-2", "some-ctx-2") + return mc + }(), }}, exec.moduleContexts, "Wrong module contexts after executing raw-bidder-response hook.") // test that context added at the auction-response stage merged with existing module contexts exec.ExecuteAuctionResponseStage(&openrtb2.BidResponse{}) assert.Nil(t, reject, "Unexpected reject from raw-auction stage.") - assert.Equal(t, &moduleContexts{ctxs: map[string]hookstage.ModuleContext{ - "module-1": { - "entrypoint-ctx-1": "some-ctx-1", - "entrypoint-ctx-3": "some-ctx-3", - "raw-auction-ctx-1": "some-ctx-1", - "raw-auction-ctx-3": "some-ctx-3", - "processed-auction-ctx-1": "some-ctx-1", - "processed-auction-ctx-3": "some-ctx-3", - "raw-bidder-response-ctx-1": "some-ctx-1", - "raw-bidder-response-ctx-3": "some-ctx-3", - "auction-response-ctx-1": "some-ctx-1", - "auction-response-ctx-3": "some-ctx-3", - }, - "module-2": { - "entrypoint-ctx-2": "some-ctx-2", - "raw-auction-ctx-2": "some-ctx-2", - "processed-auction-ctx-2": "some-ctx-2", - "raw-bidder-response-ctx-2": "some-ctx-2", - "auction-response-ctx-2": "some-ctx-2", - }, + assert.Equal(t, &moduleContexts{ctxs: map[string]*hookstage.ModuleContext{ + "module-1": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-1", "some-ctx-1") + mc.Set("entrypoint-ctx-3", "some-ctx-3") + mc.Set("raw-auction-ctx-1", "some-ctx-1") + mc.Set("raw-auction-ctx-3", "some-ctx-3") + mc.Set("processed-auction-ctx-1", "some-ctx-1") + mc.Set("processed-auction-ctx-3", "some-ctx-3") + mc.Set("raw-bidder-response-ctx-1", "some-ctx-1") + mc.Set("raw-bidder-response-ctx-3", "some-ctx-3") + mc.Set("auction-response-ctx-1", "some-ctx-1") + mc.Set("auction-response-ctx-3", "some-ctx-3") + return mc + }(), + "module-2": func() *hookstage.ModuleContext { + mc := hookstage.NewModuleContext() + mc.Set("entrypoint-ctx-2", "some-ctx-2") + mc.Set("raw-auction-ctx-2", "some-ctx-2") + mc.Set("processed-auction-ctx-2", "some-ctx-2") + mc.Set("raw-bidder-response-ctx-2", "some-ctx-2") + mc.Set("auction-response-ctx-2", "some-ctx-2") + return mc + }(), }}, exec.moduleContexts, "Wrong module contexts after executing auction-response hook.") } diff --git a/hooks/hookexecution/mocks_test.go b/hooks/hookexecution/mocks_test.go index 3d9d982861a..9e35a15093a 100644 --- a/hooks/hookexecution/mocks_test.go +++ b/hooks/hookexecution/mocks_test.go @@ -201,42 +201,50 @@ type mockModuleContextHook struct { } func (e mockModuleContextHook) HandleEntrypointHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.EntrypointPayload) (hookstage.HookResult[hookstage.EntrypointPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.EntrypointPayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleRawAuctionHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.RawAuctionRequestPayload) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.RawAuctionRequestPayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleProcessedAuctionHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.ProcessedAuctionRequestPayload) (hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.ProcessedAuctionRequestPayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleBidderRequestHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.BidderRequestPayload) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.BidderRequestPayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleRawBidderResponseHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.RawBidderResponsePayload) (hookstage.HookResult[hookstage.RawBidderResponsePayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.RawBidderResponsePayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleAllProcessedBidResponsesHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.AllProcessedBidResponsesPayload) (hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.AllProcessedBidResponsesPayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleAuctionResponseHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.AuctionResponsePayload) (hookstage.HookResult[hookstage.AuctionResponsePayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.AuctionResponsePayload]{ModuleContext: miCtx.ModuleContext}, nil } func (e mockModuleContextHook) HandleExitpointHook(_ context.Context, miCtx hookstage.ModuleInvocationContext, _ hookstage.ExitpointPayload) (hookstage.HookResult[hookstage.ExitpointPayload], error) { - miCtx.ModuleContext = map[string]interface{}{e.key: e.val} + miCtx.ModuleContext = hookstage.NewModuleContext() + miCtx.ModuleContext.Set(e.key, e.val) return hookstage.HookResult[hookstage.ExitpointPayload]{ModuleContext: miCtx.ModuleContext}, nil } diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go index b5452af7fcb..e7c958bd777 100644 --- a/hooks/hookstage/invocation.go +++ b/hooks/hookstage/invocation.go @@ -2,6 +2,7 @@ package hookstage import ( "encoding/json" + "sync" "github.com/prebid/prebid-server/v3/hooks/hookanalytics" ) @@ -16,7 +17,7 @@ type HookResult[T any] struct { Warnings []string DebugMessages []string AnalyticsTags hookanalytics.Analytics - ModuleContext ModuleContext // holds values that the module wants to pass to itself at later stages + ModuleContext *ModuleContext // holds values that the module wants to pass to itself at later stages } // ModuleInvocationContext holds data passed to the module hook during invocation. @@ -28,11 +29,75 @@ type ModuleInvocationContext struct { // Endpoint represents the path of the current endpoint. Endpoint string // ModuleContext holds values that the module passes to itself from the previous stages. - ModuleContext ModuleContext + ModuleContext *ModuleContext // HookImplCode is the hook_impl_code for a module instance to differentiate between multiple hooks HookImplCode string } // ModuleContext holds arbitrary data passed between module hooks at different stages. // We use interface as we do not know exactly how the modules will use their inner context. -type ModuleContext map[string]interface{} +type ModuleContext struct { + sync.RWMutex + data map[string]any +} + +// NewModuleContext creates a new module context +func NewModuleContext() *ModuleContext { + moduleContext := ModuleContext{ + data: make(map[string]any), + } + return &moduleContext +} + +// Get retrieves a value from the module context with read lock +func (mc *ModuleContext) Get(key string) (any, bool) { + if mc == nil || mc.data == nil { + return nil, false + } + mc.RLock() + defer mc.RUnlock() + val, ok := mc.data[key] + return val, ok +} + +// Set stores a value in the module context with write lock +func (mc *ModuleContext) Set(key string, value any) { + if mc == nil { + return + } + mc.Lock() + defer mc.Unlock() + if mc.data == nil { + mc.data = make(map[string]any) + } + mc.data[key] = value +} + +// GetAll returns a copy of all data in the context +func (mc *ModuleContext) GetAll() map[string]any { + if mc == nil || mc.data == nil { + return nil + } + mc.RLock() + defer mc.RUnlock() + result := make(map[string]any, len(mc.data)) + for k, v := range mc.data { + result[k] = v + } + return result +} + +// SetAll replaces all data in the context +func (mc *ModuleContext) SetAll(data map[string]any) { + if mc == nil { + return + } + mc.Lock() + defer mc.Unlock() + if mc.data == nil { + mc.data = make(map[string]any) + } + for k, v := range data { + mc.data[k] = v + } +} diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go index 700f53117dc..92cef33feac 100644 --- a/modules/fiftyonedegrees/devicedetection/evidence_extractor.go +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go @@ -57,16 +57,18 @@ func merge(val1, val2 []stringEvidence) []stringEvidence { return evidence } -func (x *defaultEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { +func (x *defaultEvidenceExtractor) extract(ctx *hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { if ctx == nil { return nil, "", errors.New("context is nil") } - suaStrings, err := x.getEvidenceStrings(ctx[evidenceFromSuaCtxKey]) + evidenceFromSuaCtx, _ := ctx.Get(evidenceFromSuaCtxKey) + suaStrings, err := x.getEvidenceStrings(evidenceFromSuaCtx) if err != nil { return nil, "", fmt.Errorf("error extracting sua evidence: %w", err) } - headerString, err := x.getEvidenceStrings(ctx[evidenceFromHeadersCtxKey]) + evidenceFromHeadersCtx, _ := ctx.Get(evidenceFromHeadersCtxKey) + headerString, err := x.getEvidenceStrings(evidenceFromHeadersCtx) if err != nil { return nil, "", fmt.Errorf("error extracting header evidence: %w", err) } diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go index 7dc4fb97901..08621ececb0 100644 --- a/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go @@ -129,7 +129,7 @@ func TestExtract(t *testing.T) { tests := []struct { name string - ctx hookstage.ModuleContext + ctx *hookstage.ModuleContext wantEvidenceCount int wantUserAgent string wantError bool @@ -141,96 +141,120 @@ func TestExtract(t *testing.T) { }, { name: "empty", - ctx: hookstage.ModuleContext{ - evidenceFromSuaCtxKey: []stringEvidence{}, - evidenceFromHeadersCtxKey: []stringEvidence{}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{}) + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{}) + return ctx + }(), wantEvidenceCount: 0, wantUserAgent: "", }, { name: "from_headers", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{uaEvidence1}) + return ctx + }(), wantEvidenceCount: 1, wantUserAgent: "uav1", }, { name: "from_headers_no_user_agent", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{evidence1}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{evidence1}) + return ctx + }(), wantError: true, }, { name: "from_sua", - ctx: hookstage.ModuleContext{ - evidenceFromSuaCtxKey: []stringEvidence{uaEvidence1}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{uaEvidence1}) + return ctx + }(), wantEvidenceCount: 1, wantUserAgent: "uav1", }, { name: "from_sua_no_user_agent", - ctx: hookstage.ModuleContext{ - evidenceFromSuaCtxKey: []stringEvidence{evidence1}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{evidence1}) + return ctx + }(), wantError: true, }, { name: "from_headers_error", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: "bad value", - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, "bad value") + return ctx + }(), wantError: true, }, { name: "from_sua_error", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{}, - evidenceFromSuaCtxKey: "bad value", - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{}) + ctx.Set(evidenceFromSuaCtxKey, "bad value") + return ctx + }(), wantError: true, }, { name: "from_sua_and_headers", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, - evidenceFromSuaCtxKey: []stringEvidence{evidence1}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{uaEvidence1}) + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{evidence1}) + return ctx + }(), wantEvidenceCount: 2, wantUserAgent: "uav1", }, { name: "from_sua_and_headers_sua_can_overwrite_if_ua_present", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, - evidenceFromSuaCtxKey: []stringEvidence{uaEvidence2}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{uaEvidence1}) + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{uaEvidence2}) + return ctx + }(), wantEvidenceCount: 1, wantUserAgent: "uav2", }, { name: "empty_string_values", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{emptyEvidence}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{emptyEvidence}) + return ctx + }(), wantError: true, }, { name: "empty_sua_values", - ctx: hookstage.ModuleContext{ - evidenceFromSuaCtxKey: []stringEvidence{emptyEvidence}, - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromSuaCtxKey, []stringEvidence{emptyEvidence}) + return ctx + }(), wantError: true, }, { name: "mixed_valid_and_invalid", - ctx: hookstage.ModuleContext{ - evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, - evidenceFromSuaCtxKey: "bad value", - }, + ctx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(evidenceFromHeadersCtxKey, []stringEvidence{uaEvidence1}) + ctx.Set(evidenceFromSuaCtxKey, "bad value") + return ctx + }(), wantError: true, }, } diff --git a/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go index 8be51c422df..46a0665ef00 100644 --- a/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go +++ b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go @@ -18,10 +18,10 @@ func handleAuctionEntryPointRequestHook(cfg config, payload hookstage.Entrypoint evidenceFromSua := evidenceExtractor.fromSuaPayload(payload.Body) // create a Module context and set the evidence from headers, evidence from sua and dd enabled flag - moduleContext := make(hookstage.ModuleContext) - moduleContext[evidenceFromHeadersCtxKey] = evidenceFromHeaders - moduleContext[evidenceFromSuaCtxKey] = evidenceFromSua - moduleContext[ddEnabledCtxKey] = true + moduleContext := hookstage.NewModuleContext() + moduleContext.Set(evidenceFromHeadersCtxKey, evidenceFromHeaders) + moduleContext.Set(evidenceFromSuaCtxKey, evidenceFromSua) + moduleContext.Set(ddEnabledCtxKey, true) return hookstage.HookResult[hookstage.EntrypointPayload]{ ModuleContext: moduleContext, diff --git a/modules/fiftyonedegrees/devicedetection/module.go b/modules/fiftyonedegrees/devicedetection/module.go index 603f37810dd..4256e593402 100644 --- a/modules/fiftyonedegrees/devicedetection/module.go +++ b/modules/fiftyonedegrees/devicedetection/module.go @@ -83,7 +83,7 @@ type accountValidator interface { type evidenceExtractor interface { fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence fromSuaPayload(payload []byte) []stringEvidence - extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) + extract(ctx *hookstage.ModuleContext) ([]onpremise.Evidence, string, error) } func (m Module) HandleEntrypointHook( diff --git a/modules/fiftyonedegrees/devicedetection/module_test.go b/modules/fiftyonedegrees/devicedetection/module_test.go index 19595aca4e1..0b1080bab9f 100644 --- a/modules/fiftyonedegrees/devicedetection/module_test.go +++ b/modules/fiftyonedegrees/devicedetection/module_test.go @@ -44,7 +44,7 @@ func (m *mockEvidenceExtractor) fromSuaPayload(payload []byte) []stringEvidence return args.Get(0).([]stringEvidence) } -func (m *mockEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { +func (m *mockEvidenceExtractor) extract(ctx *hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { args := m.Called(ctx) res := args.Get(0) @@ -131,16 +131,18 @@ func TestHandleEntrypointHookAccountAllowed(t *testing.T) { result, err := module.HandleEntrypointHook(nil, hookstage.ModuleInvocationContext{}, hookstage.EntrypointPayload{}) assert.NoError(t, err) + evidenceFromHeadersCtx, _ := result.ModuleContext.Get(evidenceFromHeadersCtxKey) assert.Equal( - t, result.ModuleContext[evidenceFromHeadersCtxKey], []stringEvidence{{ + t, evidenceFromHeadersCtx, []stringEvidence{{ Prefix: "123", Key: "key", Value: "val", }}, ) + evidenceFromSuaCtx, _ := result.ModuleContext.Get(evidenceFromSuaCtxKey) assert.Equal( - t, result.ModuleContext[evidenceFromSuaCtxKey], []stringEvidence{{ + t, evidenceFromSuaCtx, []stringEvidence{{ Prefix: "123", Key: "User-Agent", Value: "ua", @@ -179,9 +181,8 @@ func TestHandleRawAuctionHookExtractError(t *testing.T) { accountValidator: &mockValidator, } - mctx := make(hookstage.ModuleContext) - - mctx[ddEnabledCtxKey] = true + mctx := hookstage.NewModuleContext() + mctx.Set(ddEnabledCtxKey, true) result, err := module.HandleRawAuctionHook( context.TODO(), hookstage.ModuleInvocationContext{ @@ -280,8 +281,8 @@ func TestHandleRawAuctionHookEnrichment(t *testing.T) { accountValidator: &mockValidator, } - mctx := make(hookstage.ModuleContext) - mctx[ddEnabledCtxKey] = true + mctx := hookstage.NewModuleContext() + mctx.Set(ddEnabledCtxKey, true) result, err := module.HandleRawAuctionHook( nil, hookstage.ModuleInvocationContext{ @@ -483,8 +484,8 @@ func TestHandleRawAuctionHookEnrichmentWithErrors(t *testing.T) { accountValidator: &mockValidator, } - mctx := make(hookstage.ModuleContext) - mctx[ddEnabledCtxKey] = true + mctx := hookstage.NewModuleContext() + mctx.Set(ddEnabledCtxKey, true) result, err := module.HandleRawAuctionHook( nil, hookstage.ModuleInvocationContext{ diff --git a/modules/prebid/ortb2blocking/hook_bidderrequest.go b/modules/prebid/ortb2blocking/hook_bidderrequest.go index 4b3cb87c855..e1c8b02b006 100644 --- a/modules/prebid/ortb2blocking/hook_bidderrequest.go +++ b/modules/prebid/ortb2blocking/hook_bidderrequest.go @@ -47,7 +47,10 @@ func handleBidderRequestHook( updateCatTax(cfg, payload, &blockingAttributes, &changeSet) result.ChangeSet = changeSet - result.ModuleContext = hookstage.ModuleContext{payload.Bidder: blockingAttributes} + if result.ModuleContext == nil { + result.ModuleContext = hookstage.NewModuleContext() + } + result.ModuleContext.Set(payload.Bidder, blockingAttributes) return result, nil } diff --git a/modules/prebid/ortb2blocking/hook_raw_bidder_response.go b/modules/prebid/ortb2blocking/hook_raw_bidder_response.go index a6ddbdd58c3..4d4fdfff4bc 100644 --- a/modules/prebid/ortb2blocking/hook_raw_bidder_response.go +++ b/modules/prebid/ortb2blocking/hook_raw_bidder_response.go @@ -15,10 +15,10 @@ import ( func handleRawBidderResponseHook( cfg config, payload hookstage.RawBidderResponsePayload, - moduleCtx hookstage.ModuleContext, + moduleCtx *hookstage.ModuleContext, ) (result hookstage.HookResult[hookstage.RawBidderResponsePayload], err error) { bidder := payload.Bidder - blockAttrsVal, ok := moduleCtx[bidder] + blockAttrsVal, ok := moduleCtx.Get(bidder) if !ok { // if there are no blocking attributes for this bidder just pass empty blockingAttributes for further processing // other values from config must still be checked diff --git a/modules/prebid/ortb2blocking/module_test.go b/modules/prebid/ortb2blocking/module_test.go index 17008b2dad6..fcd8c2e9f79 100644 --- a/modules/prebid/ortb2blocking/module_test.go +++ b/modules/prebid/ortb2blocking/module_test.go @@ -342,8 +342,9 @@ func TestHandleBidderRequestHook(t *testing.T) { }, }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ - ModuleContext: map[string]interface{}{ - bidder: blockingAttributes{ + ModuleContext: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{ bAdv: []string{bAdvA, bAdvB}, bApp: []string{bApp3}, bCat: []string{bCat1, bCat2, bCat3, bCat4}, @@ -356,8 +357,9 @@ func TestHandleBidderRequestHook(t *testing.T) { "ImpID2": toInt([]adcom1.CreativeAttribute{bAttr1, bAttr8, bAttr9, bAttr10}), }, catTax: catTax, - }, - }, + }) + return mctx + }(), Warnings: []string{ // multiple warnings may be added (per condition) "More than one condition matches request. Bidder: appnexus, request media types: audio, banner, native, video", @@ -388,13 +390,15 @@ func TestHandleBidderRequestHook(t *testing.T) { }, }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ - ModuleContext: map[string]interface{}{ - bidder: blockingAttributes{ + ModuleContext: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{ bAdv: []string{bAdvA, bAdvB, bAdvC}, bType: map[string][]int{}, bAttr: map[string][]int{}, - }, - }, + }) + return mctx + }(), }, expectedError: nil, }, @@ -439,10 +443,14 @@ func TestHandleBidderRequestHook(t *testing.T) { }, }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ - ModuleContext: map[string]interface{}{bidder: blockingAttributes{ - bType: map[string][]int{}, - bAttr: map[string][]int{}, - }}, + ModuleContext: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{ + bType: map[string][]int{}, + bAttr: map[string][]int{}, + }) + return mctx + }(), }, expectedError: nil, }, @@ -460,10 +468,14 @@ func TestHandleBidderRequestHook(t *testing.T) { Imp: []openrtb2.Imp{{ID: "ImpID1", Video: &openrtb2.Video{}}}, }, expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{ - ModuleContext: map[string]interface{}{bidder: blockingAttributes{ - bType: map[string][]int{}, - bAttr: map[string][]int{}, - }}, + ModuleContext: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{ + bType: map[string][]int{}, + bAttr: map[string][]int{}, + }) + return mctx + }(), }, expectedError: nil, }, @@ -581,7 +593,7 @@ func TestHandleBidderRequestHook(t *testing.T) { hookstage.ModuleInvocationContext{ AccountConfig: test.config, Endpoint: hookexecution.EndpointAuction, - ModuleContext: map[string]interface{}{}, + ModuleContext: hookstage.NewModuleContext(), }, payload, ) @@ -607,7 +619,7 @@ func TestHandleRawBidderResponseHook(t *testing.T) { description string payload hookstage.RawBidderResponsePayload config json.RawMessage - moduleCtx hookstage.ModuleContext + moduleCtx *hookstage.ModuleContext expectedBids []*adapters.TypedBid expectedHookResult hookstage.HookResult[hookstage.RawBidderResponsePayload] expectedError error @@ -657,7 +669,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ADomain: []string{"foo"}, ImpID: impID1}, }, }, - moduleCtx: map[string]interface{}{bidder: "boo"}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, "boo") + return mctx + }(), expectedError: hookexecution.NewFailure("could not cast blocking attributes for bidder `appnexus`, module context has incorrect data"), }, { @@ -678,7 +694,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bAdv: []string{"forbidden_domain"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bAdv: []string{"forbidden_domain"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -726,7 +746,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{"other-bidder": blockingAttributes{bAdv: []string{"forbidden_domain"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set("other-bidder", blockingAttributes{bAdv: []string{"forbidden_domain"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -769,7 +793,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bAdv: []string{"forbidden_domain"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bAdv: []string{"forbidden_domain"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -812,7 +840,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bAdv: []string{"forbidden_domain"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bAdv: []string{"forbidden_domain"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -852,7 +884,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -900,7 +936,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -943,7 +983,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", ADomain: []string{"good_domain"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bAdv: []string{"forbidden_domain"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bAdv: []string{"forbidden_domain"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -1064,7 +1108,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", Cat: []string{"moto"}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bCat: []string{"fishing"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bCat: []string{"fishing"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -1190,7 +1238,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", CatTax: 2, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{catTax: 2}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{catTax: 2}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -1279,7 +1331,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", Bundle: "allowed_bundle", ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bApp: []string{"forbidden_bundle"}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bApp: []string{"forbidden_bundle"}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -1378,7 +1434,11 @@ func TestHandleRawBidderResponseHook(t *testing.T) { Bid: &openrtb2.Bid{ID: "2", Attr: []adcom1.CreativeAttribute{2}, ImpID: impID2}, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{bAttr: map[string][]int{impID1: {1}}}}, + moduleCtx: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set(bidder, blockingAttributes{bAttr: map[string][]int{impID1: {1}}}) + return mctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ @@ -1501,13 +1561,17 @@ func TestHandleRawBidderResponseHook(t *testing.T) { }, }, }, - moduleCtx: map[string]interface{}{bidder: blockingAttributes{ - bAdv: []string{"forbidden_domain"}, - bCat: []string{"fishing"}, - catTax: 2, - bApp: []string{"forbidden_bundle"}, - bAttr: map[string][]int{impID1: {1}}}, - }, + moduleCtx: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(bidder, blockingAttributes{ + bAdv: []string{"forbidden_domain"}, + bCat: []string{"fishing"}, + catTax: 2, + bApp: []string{"forbidden_bundle"}, + bAttr: map[string][]int{impID1: {1}}, + }) + return ctx + }(), expectedHookResult: hookstage.HookResult[hookstage.RawBidderResponsePayload]{ AnalyticsTags: hookanalytics.Analytics{ Activities: []hookanalytics.Activity{ diff --git a/modules/scope3/rtd/module.go b/modules/scope3/rtd/module.go index e4c19380b26..d421271ef85 100644 --- a/modules/scope3/rtd/module.go +++ b/modules/scope3/rtd/module.go @@ -194,11 +194,11 @@ func (m *Module) HandleEntrypointHook( miCtx hookstage.ModuleInvocationContext, payload hookstage.EntrypointPayload, ) (hookstage.HookResult[hookstage.EntrypointPayload], error) { + moduleContext := hookstage.NewModuleContext() + moduleContext.Set(asyncRequestKey, m.NewAsyncRequest(payload.Request)) // Initialize module context with sync.Map for thread-safe segment storage return hookstage.HookResult[hookstage.EntrypointPayload]{ - ModuleContext: hookstage.ModuleContext{ - asyncRequestKey: m.NewAsyncRequest(payload.Request), - }, + ModuleContext: moduleContext, }, nil } @@ -211,7 +211,23 @@ func (m *Module) HandleRawAuctionHook( var ret hookstage.HookResult[hookstage.RawAuctionRequestPayload] analyticsNamePrefix := "HandleRawAuctionHook." - asyncRequest, ok := miCtx.ModuleContext[asyncRequestKey].(*AsyncRequest) + request, ok := miCtx.ModuleContext.Get(asyncRequestKey) + if !ok { + // Log error but don't fail the auction + ret.AnalyticsTags = hookanalytics.Analytics{ + Activities: []hookanalytics.Activity{{ + Name: analyticsNamePrefix + asyncRequestKey, + Status: hookanalytics.ActivityStatusError, + Results: []hookanalytics.Result{{ + Status: hookanalytics.ResultStatusError, + Values: map[string]interface{}{"error": "failed to get async request from module context"}, + }}, + }}, + } + return ret, nil + } + + asyncRequest, ok := request.(*AsyncRequest) if !ok { // Log error but don't fail the auction ret.AnalyticsTags = hookanalytics.Analytics{ @@ -258,7 +274,24 @@ func (m *Module) HandleAuctionResponseHook( ) (hookstage.HookResult[hookstage.AuctionResponsePayload], error) { analyticsNamePrefix := "HandleAuctionResponseHook." var ret hookstage.HookResult[hookstage.AuctionResponsePayload] - asyncRequest, ok := miCtx.ModuleContext[asyncRequestKey].(*AsyncRequest) + + request, ok := miCtx.ModuleContext.Get(asyncRequestKey) + if !ok { + // Log error but don't fail the auction + ret.AnalyticsTags = hookanalytics.Analytics{ + Activities: []hookanalytics.Activity{{ + Name: analyticsNamePrefix + asyncRequestKey, + Status: hookanalytics.ActivityStatusError, + Results: []hookanalytics.Result{{ + Status: hookanalytics.ResultStatusError, + Values: map[string]interface{}{"error": "failed to get async request from module context"}, + }}, + }}, + } + return ret, nil + } + + asyncRequest, ok := request.(*AsyncRequest) if !ok { // Log error but don't fail the auction ret.AnalyticsTags = hookanalytics.Analytics{ diff --git a/modules/scope3/rtd/module_test.go b/modules/scope3/rtd/module_test.go index 8b46d3bbb8d..7a7091457cc 100644 --- a/modules/scope3/rtd/module_test.go +++ b/modules/scope3/rtd/module_test.go @@ -82,16 +82,19 @@ func TestHandleEntrypointHook(t *testing.T) { result, err := module.HandleEntrypointHook(ctx, miCtx, payload) assert.NoError(t, err) - assert.NotNil(t, result.ModuleContext[asyncRequestKey]) + asyncRequest, _ := result.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) } func TestHandleAuctionResponseHook_NoSegments(t *testing.T) { module := &Module{} ctx := context.Background() miCtx := hookstage.ModuleInvocationContext{ - ModuleContext: hookstage.ModuleContext{ - "segments": &sync.Map{}, - }, + ModuleContext: func() *hookstage.ModuleContext { + mctx := hookstage.NewModuleContext() + mctx.Set("segments", &sync.Map{}) + return mctx + }(), } payload := hookstage.AuctionResponsePayload{} @@ -250,7 +253,8 @@ func TestScope3APIIntegrationWithTargeting(t *testing.T) { // Test entrypoint hook entrypointResult, err := module.HandleEntrypointHook(ctx, hookstage.ModuleInvocationContext{}, getTestEntrypointPayload(t)) require.NoError(t, err) - assert.NotNil(t, entrypointResult.ModuleContext[asyncRequestKey]) + asyncRequest, _ := entrypointResult.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) // Create test request payload width := int64(300) @@ -368,7 +372,8 @@ func TestScope3APIIntegrationWithExistingPrebidTargeting(t *testing.T) { // Test entrypoint hook entrypointResult, err := module.HandleEntrypointHook(ctx, hookstage.ModuleInvocationContext{}, getTestEntrypointPayload(t)) require.NoError(t, err) - assert.NotNil(t, entrypointResult.ModuleContext[asyncRequestKey]) + asyncRequest, _ := entrypointResult.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) // Create test request payload width := int64(300) @@ -486,7 +491,8 @@ func TestScope3APIIntegrationWithExistingPrebidNoTargeting(t *testing.T) { // Test entrypoint hook entrypointResult, err := module.HandleEntrypointHook(ctx, hookstage.ModuleInvocationContext{}, getTestEntrypointPayload(t)) require.NoError(t, err) - assert.NotNil(t, entrypointResult.ModuleContext[asyncRequestKey]) + asyncRequest, _ := entrypointResult.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) // Create test request payload width := int64(300) From cbada1eb3a3cedd19325bc4ce99a6d6616623047 Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Thu, 8 Jan 2026 16:01:24 +0530 Subject: [PATCH 2/6] Switched to more performant solution while adding module contexts --- hooks/hookexecution/context.go | 6 +++++- hooks/hookstage/invocation.go | 39 +++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/hooks/hookexecution/context.go b/hooks/hookexecution/context.go index 05d8ac4fe5c..6a26d2b1331 100644 --- a/hooks/hookexecution/context.go +++ b/hooks/hookexecution/context.go @@ -59,8 +59,12 @@ func (mc *moduleContexts) put(moduleName string, mCtx *hookstage.ModuleContext) return } + if mCtx == existingCtx { + return + } + // Add new data to existing context - existingCtx.SetAll(mCtx.GetAll()) + existingCtx.Insert(mCtx.All()) } func (mc *moduleContexts) get(moduleName string) (*hookstage.ModuleContext, bool) { diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go index e7c958bd777..3a4e9f78d3b 100644 --- a/hooks/hookstage/invocation.go +++ b/hooks/hookstage/invocation.go @@ -2,6 +2,8 @@ package hookstage import ( "encoding/json" + "iter" + "maps" "sync" "github.com/prebid/prebid-server/v3/hooks/hookanalytics" @@ -81,9 +83,7 @@ func (mc *ModuleContext) GetAll() map[string]any { mc.RLock() defer mc.RUnlock() result := make(map[string]any, len(mc.data)) - for k, v := range mc.data { - result[k] = v - } + maps.Copy(result, mc.data) return result } @@ -97,7 +97,36 @@ func (mc *ModuleContext) SetAll(data map[string]any) { if mc.data == nil { mc.data = make(map[string]any) } - for k, v := range data { - mc.data[k] = v + maps.Copy(mc.data, data) +} + +var emptyMapIter iter.Seq2[string, any] = func(yield func(string, any) bool) {} + +// All returns an iterator over key-value pairs from the module context with read lock held +func (mc *ModuleContext) All() iter.Seq2[string, any] { + if mc == nil || mc.data == nil { + return emptyMapIter + } + + return func(yield func(string, any) bool) { + mc.RLock() + defer mc.RUnlock() + maps.All(mc.data)(yield) + } +} + +// Insert adds the key-value pairs from seq to the module context with write lock held +func (mc *ModuleContext) Insert(seq iter.Seq2[string, any]) { + if mc == nil { + return + } + + mc.Lock() + defer mc.Unlock() + + if mc.data == nil { + mc.data = make(map[string]any) } + + maps.Insert(mc.data, seq) } From 109bff61676545301a8bd8a4c1625779f5007adb Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Wed, 21 Jan 2026 11:47:14 +0530 Subject: [PATCH 3/6] Reverted to GetAll and SetAll methods to update the ModuleCtxs --- hooks/hookexecution/context.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hooks/hookexecution/context.go b/hooks/hookexecution/context.go index 6a26d2b1331..05d8ac4fe5c 100644 --- a/hooks/hookexecution/context.go +++ b/hooks/hookexecution/context.go @@ -59,12 +59,8 @@ func (mc *moduleContexts) put(moduleName string, mCtx *hookstage.ModuleContext) return } - if mCtx == existingCtx { - return - } - // Add new data to existing context - existingCtx.Insert(mCtx.All()) + existingCtx.SetAll(mCtx.GetAll()) } func (mc *moduleContexts) get(moduleName string) (*hookstage.ModuleContext, bool) { From 7c46e0c1e910aaf438697e6829afc22a7c362fff Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Wed, 4 Feb 2026 11:42:18 +0530 Subject: [PATCH 4/6] Removed unused methods for module context update --- hooks/hookstage/invocation.go | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go index 3a4e9f78d3b..203197d4409 100644 --- a/hooks/hookstage/invocation.go +++ b/hooks/hookstage/invocation.go @@ -2,7 +2,6 @@ package hookstage import ( "encoding/json" - "iter" "maps" "sync" @@ -95,38 +94,7 @@ func (mc *ModuleContext) SetAll(data map[string]any) { mc.Lock() defer mc.Unlock() if mc.data == nil { - mc.data = make(map[string]any) + mc.data = make(map[string]any, len(data)) } maps.Copy(mc.data, data) } - -var emptyMapIter iter.Seq2[string, any] = func(yield func(string, any) bool) {} - -// All returns an iterator over key-value pairs from the module context with read lock held -func (mc *ModuleContext) All() iter.Seq2[string, any] { - if mc == nil || mc.data == nil { - return emptyMapIter - } - - return func(yield func(string, any) bool) { - mc.RLock() - defer mc.RUnlock() - maps.All(mc.data)(yield) - } -} - -// Insert adds the key-value pairs from seq to the module context with write lock held -func (mc *ModuleContext) Insert(seq iter.Seq2[string, any]) { - if mc == nil { - return - } - - mc.Lock() - defer mc.Unlock() - - if mc.data == nil { - mc.data = make(map[string]any) - } - - maps.Insert(mc.data, seq) -} From d9a9b3dea59bef8a033f427b843a2f23cea91462 Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Wed, 25 Mar 2026 11:57:32 +0530 Subject: [PATCH 5/6] Moved data nil check inside lock protection --- hooks/hookstage/invocation.go | 12 +++++++++--- modules/scope3/rtd/module_test.go | 6 ++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/hooks/hookstage/invocation.go b/hooks/hookstage/invocation.go index c5ad68ec950..b0b1c00089e 100644 --- a/hooks/hookstage/invocation.go +++ b/hooks/hookstage/invocation.go @@ -52,11 +52,14 @@ func NewModuleContext() *ModuleContext { // Get retrieves a value from the module context with read lock func (mc *ModuleContext) Get(key string) (any, bool) { - if mc == nil || mc.data == nil { + if mc == nil { return nil, false } mc.RLock() defer mc.RUnlock() + if mc.data == nil { + return nil, false + } val, ok := mc.data[key] return val, ok } @@ -76,17 +79,20 @@ func (mc *ModuleContext) Set(key string, value any) { // GetAll returns a copy of all data in the context func (mc *ModuleContext) GetAll() map[string]any { - if mc == nil || mc.data == nil { + if mc == nil { return nil } mc.RLock() defer mc.RUnlock() + if mc.data == nil { + return nil + } result := make(map[string]any, len(mc.data)) maps.Copy(result, mc.data) return result } -// SetAll replaces all data in the context +// SetAll merges the provided data into the module context func (mc *ModuleContext) SetAll(data map[string]any) { if mc == nil { return diff --git a/modules/scope3/rtd/module_test.go b/modules/scope3/rtd/module_test.go index 6ddcc3769f2..6b40353cf07 100644 --- a/modules/scope3/rtd/module_test.go +++ b/modules/scope3/rtd/module_test.go @@ -259,7 +259,8 @@ func TestScope3APIIntegrationNoSegments(t *testing.T) { // Test entrypoint hook entrypointResult, err := module.HandleEntrypointHook(ctx, hookstage.ModuleInvocationContext{}, getTestEntrypointPayload(t)) require.NoError(t, err) - assert.NotNil(t, entrypointResult.ModuleContext[asyncRequestKey]) + asyncRequest, _ := entrypointResult.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) payload := hookstage.ProcessedAuctionRequestPayload{ Request: &openrtb_ext.RequestWrapper{ @@ -689,7 +690,8 @@ func TestScope3APIIntegrationWithExistingPrebidTargeting(t *testing.T) { // Test entrypoint hook entrypointResult, err := module.HandleEntrypointHook(ctx, hookstage.ModuleInvocationContext{}, getTestEntrypointPayload(t)) require.NoError(t, err) - assert.NotNil(t, entrypointResult.ModuleContext[asyncRequestKey]) + asyncRequest, _ := entrypointResult.ModuleContext.Get(asyncRequestKey) + assert.NotNil(t, asyncRequest) // Create test request payload width := int64(300) From 0d1d90e12c86ee45fb04c593e80d28f0ea5677ba Mon Sep 17 00:00:00 2001 From: Nikhil Vaidya Date: Thu, 16 Apr 2026 19:09:18 +0530 Subject: [PATCH 6/6] Used synced moduleCtx for device detection module --- .../wurfl_devicedetection/module.go | 10 +++-- .../wurfl_devicedetection/module_test.go | 43 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/modules/scientiamobile/wurfl_devicedetection/module.go b/modules/scientiamobile/wurfl_devicedetection/module.go index 53bb26e74d9..7cbf8d3a6db 100644 --- a/modules/scientiamobile/wurfl_devicedetection/module.go +++ b/modules/scientiamobile/wurfl_devicedetection/module.go @@ -75,8 +75,8 @@ func (m Module) HandleEntrypointHook(ctx context.Context, invocationCtx hookstag header[k] = payload.Request.Header.Get(k) } } - moduleContext := make(hookstage.ModuleContext) - moduleContext[wurflHeaderCtxKey] = header + moduleContext := hookstage.NewModuleContext() + moduleContext.Set(wurflHeaderCtxKey, header) result.ModuleContext = moduleContext return result, nil @@ -101,7 +101,11 @@ func (m Module) HandleRawAuctionHook( return result, nil } - rawHeaders, ok := invocationCtx.ModuleContext[wurflHeaderCtxKey].(map[string]string) + headers, ok := invocationCtx.ModuleContext.Get(wurflHeaderCtxKey) + if !ok { + return result, hookexecution.NewFailure("invalid module context type") + } + rawHeaders, ok := headers.(map[string]string) if !ok { return result, hookexecution.NewFailure("invalid module context type") } diff --git a/modules/scientiamobile/wurfl_devicedetection/module_test.go b/modules/scientiamobile/wurfl_devicedetection/module_test.go index 64aaefe7fde..8dc0ee48ee7 100644 --- a/modules/scientiamobile/wurfl_devicedetection/module_test.go +++ b/modules/scientiamobile/wurfl_devicedetection/module_test.go @@ -150,7 +150,8 @@ func TestHandleEntrypointHook(t *testing.T) { } else { assert.NoError(t, err) assert.NotNil(t, result.ModuleContext) - assert.Equal(t, tc.expectedModuleCtx[wurflHeaderCtxKey], result.ModuleContext[wurflHeaderCtxKey]) + headers, _ := result.ModuleContext.Get(wurflHeaderCtxKey) + assert.Equal(t, tc.expectedModuleCtx[wurflHeaderCtxKey], headers) } }) } @@ -184,11 +185,13 @@ func TestHandleRawAuctionHook(t *testing.T) { extCaps: false, }, invocationCtx: hookstage.ModuleInvocationContext{ - ModuleContext: hookstage.ModuleContext{ - wurflHeaderCtxKey: map[string]string{ + ModuleContext: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(wurflHeaderCtxKey, map[string]string{ "User-Agent": "Mozilla/5.0", - }, - }, + }) + return ctx + }(), }, payload: []byte(`{"device":{"ua":"Mozilla/5.0"}}`), expectedErr: false, @@ -242,11 +245,13 @@ func TestHandleRawAuctionHook(t *testing.T) { extCaps: true, }, invocationCtx: hookstage.ModuleInvocationContext{ - ModuleContext: hookstage.ModuleContext{ - wurflHeaderCtxKey: map[string]string{ + ModuleContext: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(wurflHeaderCtxKey, map[string]string{ "User-Agent": "Mozilla/5.0", - }, - }, + }) + return ctx + }(), }, payload: []byte(`{"device":{"ua":"Mozilla/5.0"}}`), expectedErr: false, @@ -288,11 +293,13 @@ func TestHandleRawAuctionHook(t *testing.T) { extCaps: true, }, invocationCtx: hookstage.ModuleInvocationContext{ - ModuleContext: hookstage.ModuleContext{ - wurflHeaderCtxKey: map[string]string{ + ModuleContext: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(wurflHeaderCtxKey, map[string]string{ "User-Agent": "Mozilla/5.0", - }, - }, + }) + return ctx + }(), }, payload: []byte(`{"device":{"ua":"Mozilla/5.0", "ext": {"test": 1}}}`), expectedErr: false, @@ -328,11 +335,13 @@ func TestHandleRawAuctionHook(t *testing.T) { extCaps: false, }, invocationCtx: hookstage.ModuleInvocationContext{ - ModuleContext: hookstage.ModuleContext{ - wurflHeaderCtxKey: map[string]string{ + ModuleContext: func() *hookstage.ModuleContext { + ctx := hookstage.NewModuleContext() + ctx.Set(wurflHeaderCtxKey, map[string]string{ "User-Agent": "Mozilla/5.0", - }, - }, + }) + return ctx + }(), }, payload: []byte(`{"device":{"ua":"Mozilla/5.0"}}`), expectedErr: false,