github.com/prebid/prebid-server/v2@v2.18.0/hooks/hookexecution/execution.go (about) 1 package hookexecution 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/prebid/prebid-server/v2/config" 11 "github.com/prebid/prebid-server/v2/hooks" 12 "github.com/prebid/prebid-server/v2/hooks/hookstage" 13 "github.com/prebid/prebid-server/v2/metrics" 14 "github.com/prebid/prebid-server/v2/openrtb_ext" 15 "github.com/prebid/prebid-server/v2/ortb" 16 "github.com/prebid/prebid-server/v2/privacy" 17 "github.com/prebid/prebid-server/v2/util/iputil" 18 ) 19 20 type hookResponse[T any] struct { 21 Err error 22 ExecutionTime time.Duration 23 HookID HookID 24 Result hookstage.HookResult[T] 25 } 26 27 type hookHandler[H any, P any] func( 28 context.Context, 29 hookstage.ModuleInvocationContext, 30 H, 31 P, 32 ) (hookstage.HookResult[P], error) 33 34 func executeStage[H any, P any]( 35 executionCtx executionContext, 36 plan hooks.Plan[H], 37 payload P, 38 hookHandler hookHandler[H, P], 39 metricEngine metrics.MetricsEngine, 40 ) (StageOutcome, P, stageModuleContext, *RejectError) { 41 stageOutcome := StageOutcome{} 42 stageOutcome.Groups = make([]GroupOutcome, 0, len(plan)) 43 stageModuleCtx := stageModuleContext{} 44 stageModuleCtx.groupCtx = make([]groupModuleContext, 0, len(plan)) 45 46 for _, group := range plan { 47 groupOutcome, newPayload, moduleContexts, rejectErr := executeGroup(executionCtx, group, payload, hookHandler, metricEngine) 48 stageOutcome.ExecutionTimeMillis += groupOutcome.ExecutionTimeMillis 49 stageOutcome.Groups = append(stageOutcome.Groups, groupOutcome) 50 stageModuleCtx.groupCtx = append(stageModuleCtx.groupCtx, moduleContexts) 51 if rejectErr != nil { 52 return stageOutcome, payload, stageModuleCtx, rejectErr 53 } 54 55 payload = newPayload 56 } 57 58 return stageOutcome, payload, stageModuleCtx, nil 59 } 60 61 func executeGroup[H any, P any]( 62 executionCtx executionContext, 63 group hooks.Group[H], 64 payload P, 65 hookHandler hookHandler[H, P], 66 metricEngine metrics.MetricsEngine, 67 ) (GroupOutcome, P, groupModuleContext, *RejectError) { 68 var wg sync.WaitGroup 69 rejected := make(chan struct{}) 70 resp := make(chan hookResponse[P]) 71 72 for _, hook := range group.Hooks { 73 mCtx := executionCtx.getModuleContext(hook.Module) 74 newPayload := handleModuleActivities(hook.Code, executionCtx.activityControl, payload, executionCtx.account) 75 wg.Add(1) 76 go func(hw hooks.HookWrapper[H], moduleCtx hookstage.ModuleInvocationContext) { 77 defer wg.Done() 78 executeHook(moduleCtx, hw, newPayload, hookHandler, group.Timeout, resp, rejected) 79 }(hook, mCtx) 80 } 81 82 go func() { 83 wg.Wait() 84 close(resp) 85 }() 86 87 hookResponses := collectHookResponses(resp, rejected) 88 89 return handleHookResponses(executionCtx, hookResponses, payload, metricEngine) 90 } 91 92 func executeHook[H any, P any]( 93 moduleCtx hookstage.ModuleInvocationContext, 94 hw hooks.HookWrapper[H], 95 payload P, 96 hookHandler hookHandler[H, P], 97 timeout time.Duration, 98 resp chan<- hookResponse[P], 99 rejected <-chan struct{}, 100 ) { 101 hookRespCh := make(chan hookResponse[P], 1) 102 startTime := time.Now() 103 hookId := HookID{ModuleCode: hw.Module, HookImplCode: hw.Code} 104 105 go func() { 106 ctx, cancel := context.WithTimeout(context.Background(), timeout) 107 defer cancel() 108 result, err := hookHandler(ctx, moduleCtx, hw.Hook, payload) 109 hookRespCh <- hookResponse[P]{ 110 Result: result, 111 Err: err, 112 } 113 }() 114 115 select { 116 case res := <-hookRespCh: 117 res.HookID = hookId 118 res.ExecutionTime = time.Since(startTime) 119 resp <- res 120 case <-time.After(timeout): 121 resp <- hookResponse[P]{ 122 Err: TimeoutError{}, 123 ExecutionTime: time.Since(startTime), 124 HookID: hookId, 125 Result: hookstage.HookResult[P]{}, 126 } 127 case <-rejected: 128 return 129 } 130 } 131 132 func collectHookResponses[P any](resp <-chan hookResponse[P], rejected chan<- struct{}) []hookResponse[P] { 133 hookResponses := make([]hookResponse[P], 0) 134 for r := range resp { 135 hookResponses = append(hookResponses, r) 136 if r.Result.Reject { 137 close(rejected) 138 break 139 } 140 } 141 142 return hookResponses 143 } 144 145 func handleHookResponses[P any]( 146 executionCtx executionContext, 147 hookResponses []hookResponse[P], 148 payload P, 149 metricEngine metrics.MetricsEngine, 150 ) (GroupOutcome, P, groupModuleContext, *RejectError) { 151 groupOutcome := GroupOutcome{} 152 groupOutcome.InvocationResults = make([]HookOutcome, 0, len(hookResponses)) 153 groupModuleCtx := make(groupModuleContext, len(hookResponses)) 154 155 for _, r := range hookResponses { 156 groupModuleCtx[r.HookID.ModuleCode] = r.Result.ModuleContext 157 if r.ExecutionTime > groupOutcome.ExecutionTimeMillis { 158 groupOutcome.ExecutionTimeMillis = r.ExecutionTime 159 } 160 161 updatedPayload, hookOutcome, rejectErr := handleHookResponse(executionCtx, payload, r, metricEngine) 162 groupOutcome.InvocationResults = append(groupOutcome.InvocationResults, hookOutcome) 163 payload = updatedPayload 164 165 if rejectErr != nil { 166 return groupOutcome, payload, groupModuleCtx, rejectErr 167 } 168 } 169 170 return groupOutcome, payload, groupModuleCtx, nil 171 } 172 173 // moduleReplacer changes unwanted symbols to be in compliance with metric naming requirements 174 var moduleReplacer = strings.NewReplacer(".", "_", "-", "_") 175 176 // handleHookResponse is a strategy function that selects and applies 177 // one of the available algorithms to handle hook response. 178 func handleHookResponse[P any]( 179 ctx executionContext, 180 payload P, 181 hr hookResponse[P], 182 metricEngine metrics.MetricsEngine, 183 ) (P, HookOutcome, *RejectError) { 184 var rejectErr *RejectError 185 labels := metrics.ModuleLabels{Module: moduleReplacer.Replace(hr.HookID.ModuleCode), Stage: ctx.stage, AccountID: ctx.accountID} 186 metricEngine.RecordModuleCalled(labels, hr.ExecutionTime) 187 188 hookOutcome := HookOutcome{ 189 Status: StatusSuccess, 190 HookID: hr.HookID, 191 Message: hr.Result.Message, 192 Errors: hr.Result.Errors, 193 Warnings: hr.Result.Warnings, 194 DebugMessages: hr.Result.DebugMessages, 195 AnalyticsTags: hr.Result.AnalyticsTags, 196 ExecutionTime: ExecutionTime{ExecutionTimeMillis: hr.ExecutionTime}, 197 } 198 199 if hr.Err != nil || hr.Result.Reject { 200 handleHookError(hr, &hookOutcome, metricEngine, labels) 201 rejectErr = handleHookReject(ctx, hr, &hookOutcome, metricEngine, labels) 202 } else { 203 payload = handleHookMutations(payload, hr, &hookOutcome, metricEngine, labels) 204 } 205 206 return payload, hookOutcome, rejectErr 207 } 208 209 // handleHookError sets an appropriate status to HookOutcome depending on the type of hook execution error. 210 func handleHookError[P any]( 211 hr hookResponse[P], 212 hookOutcome *HookOutcome, 213 metricEngine metrics.MetricsEngine, 214 labels metrics.ModuleLabels, 215 ) { 216 if hr.Err == nil { 217 return 218 } 219 220 hookOutcome.Errors = append(hookOutcome.Errors, hr.Err.Error()) 221 switch hr.Err.(type) { 222 case TimeoutError: 223 metricEngine.RecordModuleTimeout(labels) 224 hookOutcome.Status = StatusTimeout 225 case FailureError: 226 metricEngine.RecordModuleFailed(labels) 227 hookOutcome.Status = StatusFailure 228 default: 229 metricEngine.RecordModuleExecutionError(labels) 230 hookOutcome.Status = StatusExecutionFailure 231 } 232 } 233 234 // handleHookReject rejects execution at the current stage. 235 // In case the stage does not support rejection, hook execution marked as failed. 236 func handleHookReject[P any]( 237 ctx executionContext, 238 hr hookResponse[P], 239 hookOutcome *HookOutcome, 240 metricEngine metrics.MetricsEngine, 241 labels metrics.ModuleLabels, 242 ) *RejectError { 243 if !hr.Result.Reject { 244 return nil 245 } 246 247 stage := hooks.Stage(ctx.stage) 248 if !stage.IsRejectable() { 249 metricEngine.RecordModuleExecutionError(labels) 250 hookOutcome.Status = StatusExecutionFailure 251 hookOutcome.Errors = append( 252 hookOutcome.Errors, 253 fmt.Sprintf( 254 "Module (name: %s, hook code: %s) tried to reject request on the %s stage that does not support rejection", 255 hr.HookID.ModuleCode, 256 hr.HookID.HookImplCode, 257 ctx.stage, 258 ), 259 ) 260 return nil 261 } 262 263 rejectErr := &RejectError{NBR: hr.Result.NbrCode, Hook: hr.HookID, Stage: ctx.stage} 264 hookOutcome.Action = ActionReject 265 hookOutcome.Errors = append(hookOutcome.Errors, rejectErr.Error()) 266 metricEngine.RecordModuleSuccessRejected(labels) 267 268 return rejectErr 269 } 270 271 // handleHookMutations applies mutations returned by hook to provided payload. 272 func handleHookMutations[P any]( 273 payload P, 274 hr hookResponse[P], 275 hookOutcome *HookOutcome, 276 metricEngine metrics.MetricsEngine, 277 labels metrics.ModuleLabels, 278 ) P { 279 if len(hr.Result.ChangeSet.Mutations()) == 0 { 280 metricEngine.RecordModuleSuccessNooped(labels) 281 hookOutcome.Action = ActionNone 282 return payload 283 } 284 285 hookOutcome.Action = ActionUpdate 286 successfulMutations := 0 287 for _, mut := range hr.Result.ChangeSet.Mutations() { 288 p, err := mut.Apply(payload) 289 if err != nil { 290 hookOutcome.Warnings = append( 291 hookOutcome.Warnings, 292 fmt.Sprintf("failed to apply hook mutation: %s", err), 293 ) 294 continue 295 } 296 297 payload = p 298 hookOutcome.DebugMessages = append( 299 hookOutcome.DebugMessages, 300 fmt.Sprintf( 301 "Hook mutation successfully applied, affected key: %s, mutation type: %s", 302 strings.Join(mut.Key(), "."), 303 mut.Type(), 304 ), 305 ) 306 successfulMutations++ 307 } 308 309 // if at least one mutation from a given module was successfully applied 310 // we consider that the module was processed successfully 311 if successfulMutations > 0 { 312 metricEngine.RecordModuleSuccessUpdated(labels) 313 } else { 314 hookOutcome.Status = StatusExecutionFailure 315 metricEngine.RecordModuleExecutionError(labels) 316 } 317 318 return payload 319 } 320 321 func handleModuleActivities[P any](hookCode string, activityControl privacy.ActivityControl, payload P, account *config.Account) P { 322 payloadData, ok := any(&payload).(hookstage.RequestUpdater) 323 if !ok { 324 return payload 325 } 326 327 scopeGeneral := privacy.Component{Type: privacy.ComponentTypeGeneral, Name: hookCode} 328 transmitUserFPDActivityAllowed := activityControl.Allow(privacy.ActivityTransmitUserFPD, scopeGeneral, privacy.ActivityRequest{}) 329 transmitPreciseGeoActivityAllowed := activityControl.Allow(privacy.ActivityTransmitPreciseGeo, scopeGeneral, privacy.ActivityRequest{}) 330 331 if transmitUserFPDActivityAllowed && transmitPreciseGeoActivityAllowed { 332 return payload 333 } 334 335 // changes need to be applied to new payload and leave original payload unchanged 336 bidderReq := payloadData.GetBidderRequestPayload() 337 338 bidderReqCopy := &openrtb_ext.RequestWrapper{ 339 BidRequest: ortb.CloneBidRequestPartial(bidderReq.BidRequest), 340 } 341 342 if !transmitUserFPDActivityAllowed { 343 privacy.ScrubUserFPD(bidderReqCopy) 344 } 345 if !transmitPreciseGeoActivityAllowed { 346 ipConf := privacy.IPConf{} 347 if account != nil { 348 ipConf = privacy.IPConf{IPV6: account.Privacy.IPv6Config, IPV4: account.Privacy.IPv4Config} 349 } else { 350 ipConf = privacy.IPConf{ 351 IPV6: config.IPv6{AnonKeepBits: iputil.IPv6DefaultMaskingBitSize}, 352 IPV4: config.IPv4{AnonKeepBits: iputil.IPv4DefaultMaskingBitSize}} 353 } 354 355 privacy.ScrubGeoAndDeviceIP(bidderReqCopy, ipConf) 356 } 357 358 var newPayload = payload 359 var np = any(&newPayload).(hookstage.RequestUpdater) 360 np.SetBidderRequestPayload(bidderReqCopy) 361 return newPayload 362 363 }