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  }