[docx] split commit for file 3200

Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
Ari Archer 2024-01-23 19:12:27 +02:00
parent 23cec12533
commit f3c5ff35cb
No known key found for this signature in database
GPG Key ID: A50D5B4B599AF8A2
400 changed files with 0 additions and 14083 deletions

View File

@ -1,57 +0,0 @@
package com.twitter.product_mixer.core.pipeline
import com.twitter.product_mixer.core.pipeline.pipeline_failure.MalformedCursor
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.scrooge.BinaryThriftStructSerializer
import com.twitter.scrooge.ThriftStruct
import com.twitter.util.Return
import com.twitter.util.Throw
import com.twitter.util.Try
/**
* Serializes a [[PipelineCursor]] into thrift and then into a base64 encoded string
*/
trait PipelineCursorSerializer[-Cursor <: PipelineCursor] {
def serializeCursor(cursor: Cursor): String
}
object PipelineCursorSerializer {
/**
* Deserializes a cursor string into thrift and then into a [[PipelineCursor]]
*
* @param cursorString to deserialize, which is base64 encoded thrift
* @param cursorThriftSerializer to deserialize the cursor string into thrift
* @param deserializePf specifies how to transform the serialized thrift into a [[PipelineCursor]]
* @return optional [[PipelineCursor]]. `None` may or may not be a failure depending on the
* implementation of deserializePf.
*
* @note The "A" type of deserializePf cannot be inferred due to the thrift type not being present
* on the PipelineCursorSerializer trait. Therefore invokers must often add an explicit type
* on the deserializeCursor call to help out the compiler when passing deserializePf inline.
* Alternatively, deserializePf can be declared as a val with a type annotation before it is
* passed into this method.
*/
def deserializeCursor[Thrift <: ThriftStruct, Cursor <: PipelineCursor](
cursorString: String,
cursorThriftSerializer: BinaryThriftStructSerializer[Thrift],
deserializePf: PartialFunction[Option[Thrift], Option[Cursor]]
): Option[Cursor] = {
val thriftCursor: Option[Thrift] =
Try {
cursorThriftSerializer.fromString(cursorString)
} match {
case Return(thriftCursor) => Some(thriftCursor)
case Throw(_) => None
}
// Add type annotation to help out the compiler since the type is lost due to the _ match
val defaultDeserializePf: PartialFunction[Option[Thrift], Option[Cursor]] = {
case _ =>
// This case is the result of the client submitting a cursor we do not expect
throw PipelineFailure(MalformedCursor, s"Unknown request cursor: $cursorString")
}
(deserializePf orElse defaultDeserializePf)(thriftCursor)
}
}

View File

@ -1,32 +0,0 @@
package com.twitter.product_mixer.core.pipeline
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext
import com.twitter.product_mixer.core.model.marshalling.request.HasDebugOptions
import com.twitter.product_mixer.core.model.marshalling.request.HasProduct
import com.twitter.timelines.configapi.HasParams
import com.twitter.timelines.configapi.Param
import com.twitter.util.Time
trait PipelineQuery extends HasParams with HasClientContext with HasProduct with HasDebugOptions {
self =>
/** Set a query time val that is constant for the duration of the query lifecycle */
val queryTime: Time = self.debugOptions.flatMap(_.requestTimeOverride).getOrElse(Time.now)
/** The requested max results is specified, or not specified, by the thrift client */
def requestedMaxResults: Option[Int]
/** Retrieves the max results with a default Param, if not specified by the thrift client */
def maxResults(defaultRequestedMaxResultParam: Param[Int]): Int =
requestedMaxResults.getOrElse(params(defaultRequestedMaxResultParam))
/** Optional [[FeatureMap]], this may be updated later using [[withFeatureMap]] */
def features: Option[FeatureMap]
/**
* Since Query-Level features can be hydrated later, we need this method to update the PipelineQuery
* usually this will be implemented via `copy(features = Some(features))`
*/
def withFeatureMap(features: FeatureMap): PipelineQuery
}

View File

@ -1,59 +0,0 @@
package com.twitter.product_mixer.core.pipeline
import com.twitter.product_mixer.component_library.model.candidate.CursorCandidate
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.model.common.presentation.ItemCandidateWithDetails
import com.twitter.product_mixer.core.model.common.presentation.ModuleCandidateWithDetails
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ExecutionFailed
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.util.Return
import com.twitter.util.Throw
import com.twitter.util.Try
/**
* Pipelines return a PipelineResult.
*
* This allows us to return a single main result (optionally, incase the pipeline didn't execute successfully), but
* still have a detailed response object to show how that result was produced.
*/
trait PipelineResult[ResultType] {
val failure: Option[PipelineFailure]
val result: Option[ResultType]
def withFailure(failure: PipelineFailure): PipelineResult[ResultType]
def withResult(result: ResultType): PipelineResult[ResultType]
def resultSize(): Int
private[pipeline] def stopExecuting: Boolean = failure.isDefined || result.isDefined
final def toTry: Try[this.type] = (result, failure) match {
case (_, Some(failure)) =>
Throw(failure)
case (_: Some[ResultType], _) =>
Return(this)
// Pipelines should always finish with either a result or a failure
case _ => Throw(PipelineFailure(ExecutionFailed, "Pipeline did not execute"))
}
final def toResultTry: Try[ResultType] = {
// `.get` is safe here because `toTry` guarantees a value in the `Return` case
toTry.map(_.result.get)
}
}
object PipelineResult {
/**
* Track number of candidates returned by a Pipeline. Cursors are excluded from this
* count and modules are counted as the sum of their candidates.
*
* @note this is a somewhat subjective measure of 'size' and it is spread across pipeline
* definitions as well as selectors.
*/
def resultSize(results: Seq[CandidateWithDetails]): Int = results.map {
case module: ModuleCandidateWithDetails => resultSize(module.candidates)
case ItemCandidateWithDetails(_: CursorCandidate, _, _) => 0
case _ => 1
}.sum
}

View File

@ -1,77 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/javax/inject:javax.inject",
"configapi/configapi-core",
"configapi/configapi-decider",
"finatra/inject/inject-core/src/main/scala",
"finatra/inject/inject-core/src/main/scala/com/twitter/inject",
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featurestorev1",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/decorator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/filter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/transformer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/presentation",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/async_feature_map_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_decorator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_source_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/filter_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/group_results_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/util",
"stitch/stitch-core",
"util/util-core:scala",
],
exports = [
"3rdparty/jvm/javax/inject:javax.inject",
"configapi/configapi-core",
"configapi/configapi-decider",
"finatra/inject/inject-core/src/main/scala",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/decorator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/filter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/transformer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/presentation",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/async_feature_map_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_decorator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_source_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/filter_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/group_results_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"stitch/stitch-core",
],
)

View File

@ -1,30 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.stitch.Arrow
/**
* A Candidate Pipeline
*
* This is an abstract class, as we only construct these via the [[CandidatePipelineBuilder]].
*
* A [[CandidatePipeline]] is capable of processing requests (queries) and returning candidates
* in the form of a [[CandidatePipelineResult]]
*
* @tparam Query the domain model for the query or request
*/
abstract class CandidatePipeline[-Query <: PipelineQuery] private[candidate]
extends Pipeline[CandidatePipeline.Inputs[Query], Seq[CandidateWithDetails]] {
override private[core] val config: BaseCandidatePipelineConfig[Query, _, _, _]
override val arrow: Arrow[CandidatePipeline.Inputs[Query], CandidatePipelineResult]
override val identifier: CandidatePipelineIdentifier
}
object CandidatePipeline {
case class Inputs[+Query <: PipelineQuery](
query: Query,
existingCandidates: Seq[CandidateWithDetails])
}

View File

@ -1,735 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseQueryFeatureHydrator
import com.twitter.product_mixer.core.functional_component.transformer.BaseCandidatePipelineQueryTransformer
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
import com.twitter.product_mixer.core.gate.ParamGate
import com.twitter.product_mixer.core.gate.ParamGate.EnabledGateSuffix
import com.twitter.product_mixer.core.gate.ParamGate.SupportedClientGateSuffix
import com.twitter.product_mixer.core.model.common.CandidateWithFeatures
import com.twitter.product_mixer.core.model.common.Component
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.pipeline.InvalidStepStateException
import com.twitter.product_mixer.core.pipeline.PipelineBuilder
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ClosedGate
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailureClassifier
import com.twitter.product_mixer.core.service.Executor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutorResults
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutor
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_source_executor.CandidateSourceExecutor
import com.twitter.product_mixer.core.service.candidate_source_executor.CandidateSourceExecutorResult
import com.twitter.product_mixer.core.service.candidate_source_executor.FetchedCandidateWithFeatures
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutor
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.StoppedGateException
import com.twitter.product_mixer.core.service.group_results_executor.GroupResultsExecutor
import com.twitter.product_mixer.core.service.group_results_executor.GroupResultsExecutorInput
import com.twitter.product_mixer.core.service.group_results_executor.GroupResultsExecutorResult
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.stitch.Arrow
import com.twitter.util.logging.Logging
import javax.inject.Inject
class CandidatePipelineBuilder[
Query <: PipelineQuery,
CandidateSourceQuery,
CandidateSourceResult,
Result <: UniversalNoun[Any]] @Inject() (
queryFeatureHydratorExecutor: QueryFeatureHydratorExecutor,
asyncFeatureMapExecutor: AsyncFeatureMapExecutor,
candidateDecoratorExecutor: CandidateDecoratorExecutor,
candidateFeatureHydratorExecutor: CandidateFeatureHydratorExecutor,
candidateSourceExecutor: CandidateSourceExecutor,
groupResultsExecutor: GroupResultsExecutor,
filterExecutor: FilterExecutor,
gateExecutor: GateExecutor,
override val statsReceiver: StatsReceiver)
extends PipelineBuilder[CandidatePipeline.Inputs[Query]]
with Logging {
override type UnderlyingResultType = Seq[CandidateWithDetails]
override type PipelineResultType = IntermediateCandidatePipelineResult[Result]
def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
config: BaseCandidatePipelineConfig[
Query,
CandidateSourceQuery,
CandidateSourceResult,
Result
]
): CandidatePipeline[Query] = {
val pipelineIdentifier = config.identifier
val candidateSourceIdentifier = config.candidateSource.identifier
val context = Executor.Context(
PipelineFailureClassifier(
config.failureClassifier.orElse(StoppedGateException.classifier(ClosedGate))),
parentComponentIdentifierStack.push(pipelineIdentifier)
)
val enabledGateOpt = config.enabledDeciderParam.map { deciderParam =>
ParamGate(pipelineIdentifier + EnabledGateSuffix, deciderParam)
}
val supportedClientGateOpt = config.supportedClientParam.map { param =>
ParamGate(pipelineIdentifier + SupportedClientGateSuffix, param)
}
/**
* Evaluate enabled decider gate first since if it's off, there is no reason to proceed
* Next evaluate supported client feature switch gate, followed by customer configured gates
*/
val allGates = enabledGateOpt.toSeq ++ supportedClientGateOpt.toSeq ++ config.gates
// Dynamically replace the identifier of both transformers if config used the inline constructor
// which sets a default identifier. We need to do this to ensure uniqueness of identifiers.
val queryTransformer = BaseCandidatePipelineQueryTransformer.copyWithUpdatedIdentifier(
config.queryTransformer,
pipelineIdentifier)
val resultsTransformer = CandidatePipelineResultsTransformer.copyWithUpdatedIdentifier(
config.resultTransformer,
pipelineIdentifier)
val decorator = config.decorator.map(decorator =>
CandidateDecorator.copyWithUpdatedIdentifier(decorator, pipelineIdentifier))
val GatesStep = new Step[Query, GateExecutorResult] {
override def identifier: PipelineStepIdentifier = CandidatePipelineConfig.gatesStep
override def executorArrow: Arrow[Query, GateExecutorResult] = {
gateExecutor.arrow(allGates, context)
}
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): Query =
query.query
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: GateExecutorResult
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(underlyingResult =
previousPipelineResult.underlyingResult.copy(gateResult = Some(executorResult)))
}
def queryFeatureHydrationStep(
queryFeatureHydrators: Seq[BaseQueryFeatureHydrator[Query, _]],
stepIdentifier: PipelineStepIdentifier,
updater: ResultUpdater[CandidatePipelineResult, QueryFeatureHydratorExecutor.Result]
): Step[Query, QueryFeatureHydratorExecutor.Result] =
new Step[Query, QueryFeatureHydratorExecutor.Result] {
override def identifier: PipelineStepIdentifier = stepIdentifier
override def executorArrow: Arrow[Query, QueryFeatureHydratorExecutor.Result] =
queryFeatureHydratorExecutor.arrow(
queryFeatureHydrators,
CandidatePipelineConfig.stepsAsyncFeatureHydrationCanBeCompletedBy,
context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): Query = query.query
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: QueryFeatureHydratorExecutor.Result
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(
underlyingResult = updater(previousPipelineResult.underlyingResult, executorResult))
override def queryUpdater(
query: CandidatePipeline.Inputs[Query],
executorResult: QueryFeatureHydratorExecutor.Result
): CandidatePipeline.Inputs[Query] =
CandidatePipeline.Inputs(
query.query
.withFeatureMap(
query.query.features.getOrElse(
FeatureMap.empty) ++ executorResult.featureMap).asInstanceOf[Query],
query.existingCandidates)
}
def asyncFeaturesStep(
stepToHydrateFor: PipelineStepIdentifier,
context: Executor.Context
): Step[AsyncFeatureMap, AsyncFeatureMapExecutorResults] =
new Step[AsyncFeatureMap, AsyncFeatureMapExecutorResults] {
override def identifier: PipelineStepIdentifier =
CandidatePipelineConfig.asyncFeaturesStep(stepToHydrateFor)
override def executorArrow: Arrow[AsyncFeatureMap, AsyncFeatureMapExecutorResults] =
asyncFeatureMapExecutor.arrow(stepToHydrateFor, identifier, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): AsyncFeatureMap =
previousResult.underlyingResult.mergedAsyncQueryFeatures
.getOrElse(
throw InvalidStepStateException(identifier, "MergedAsyncQueryFeatures")
)
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: AsyncFeatureMapExecutorResults
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(
underlyingResult =
previousPipelineResult.underlyingResult.copy(asyncFeatureHydrationResults =
previousPipelineResult.underlyingResult.asyncFeatureHydrationResults match {
case Some(existingResults) => Some(existingResults ++ executorResult)
case None => Some(executorResult)
}))
override def queryUpdater(
query: CandidatePipeline.Inputs[Query],
executorResult: AsyncFeatureMapExecutorResults
): CandidatePipeline.Inputs[Query] =
if (executorResult.featureMapsByStep
.getOrElse(stepToHydrateFor, FeatureMap.empty).isEmpty) {
query
} else {
val updatedQuery = query.query
.withFeatureMap(
query.query.features
.getOrElse(FeatureMap.empty) ++ executorResult.featureMapsByStep(
stepToHydrateFor)).asInstanceOf[Query]
CandidatePipeline.Inputs(updatedQuery, query.existingCandidates)
}
}
val CandidateSourceStep =
new Step[Query, CandidateSourceExecutorResult[Result]] {
override def identifier: PipelineStepIdentifier =
CandidatePipelineConfig.candidateSourceStep
override def executorArrow: Arrow[
Query,
CandidateSourceExecutorResult[Result]
] =
candidateSourceExecutor
.arrow(
config.candidateSource,
queryTransformer,
resultsTransformer,
config.featuresFromCandidateSourceTransformers,
context
)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): Query =
query.query
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateSourceExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(underlyingResult =
previousPipelineResult.underlyingResult.copy(
candidateSourceResult =
Some(executorResult.asInstanceOf[CandidateSourceExecutorResult[UniversalNoun[Any]]])
))
override def queryUpdater(
query: CandidatePipeline.Inputs[Query],
executorResult: CandidateSourceExecutorResult[Result]
): CandidatePipeline.Inputs[Query] = {
val updatedFeatureMap =
query.query.features
.getOrElse(FeatureMap.empty) ++ executorResult.candidateSourceFeatureMap
val updatedQuery = query.query
.withFeatureMap(updatedFeatureMap).asInstanceOf[Query]
CandidatePipeline.Inputs(updatedQuery, query.existingCandidates)
}
}
val PreFilterFeatureHydrationPhase1Step =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] {
override def identifier: PipelineStepIdentifier =
CandidatePipelineConfig.preFilterFeatureHydrationPhase1Step
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] =
candidateFeatureHydratorExecutor.arrow(config.preFilterFeatureHydrationPhase1, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): CandidateFeatureHydratorExecutor.Inputs[Query, Result] = {
val candidateSourceExecutorResult =
previousResult.underlyingResult.candidateSourceResult.getOrElse {
throw InvalidStepStateException(identifier, "CandidateSourceResult")
}
CandidateFeatureHydratorExecutor.Inputs(
query.query,
candidateSourceExecutorResult.candidates
.asInstanceOf[Seq[CandidateWithFeatures[Result]]])
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateFeatureHydratorExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] = {
val candidateSourceExecutorResult =
previousPipelineResult.underlyingResult.candidateSourceResult.getOrElse {
throw InvalidStepStateException(identifier, "CandidateSourceResult")
}
val featureMapsFromPreFilter = executorResult.results.map { result =>
result.candidate -> result.features
}.toMap
val mergedFeatureMaps = candidateSourceExecutorResult.candidates.map { candidate =>
val candidateFeatureMap = candidate.features
val preFilterFeatureMap =
featureMapsFromPreFilter.getOrElse(
candidate.candidate.asInstanceOf[Result],
FeatureMap.empty)
candidate.candidate.asInstanceOf[Result] -> (candidateFeatureMap ++ preFilterFeatureMap)
}.toMap
previousPipelineResult.copy(
underlyingResult = previousPipelineResult.underlyingResult.copy(
preFilterHydrationResult = Some(
executorResult
.asInstanceOf[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]])
),
featureMaps = Some(mergedFeatureMaps)
)
}
}
val PreFilterFeatureHydrationPhase2Step =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] {
override def identifier: PipelineStepIdentifier =
CandidatePipelineConfig.preFilterFeatureHydrationPhase2Step
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] =
candidateFeatureHydratorExecutor.arrow(config.preFilterFeatureHydrationPhase2, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): CandidateFeatureHydratorExecutor.Inputs[Query, Result] = {
val candidates = previousResult.underlyingResult.preFilterHydrationResult.getOrElse {
throw InvalidStepStateException(identifier, "PreFilterHydrationResult")
}.results
CandidateFeatureHydratorExecutor.Inputs(
query.query,
candidates.asInstanceOf[Seq[CandidateWithFeatures[Result]]]
)
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateFeatureHydratorExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] = {
val featureMapsFromPreFilterPhase2 = executorResult.results.map { result =>
result.candidate -> result.features
}.toMap
val mergedFeatureMaps = previousPipelineResult.featureMaps
.getOrElse(throw InvalidStepStateException(identifier, "FeatureMaps"))
.map {
case (candidate, featureMap) =>
val preFilterPhase2FeatureMap =
featureMapsFromPreFilterPhase2.getOrElse(candidate, FeatureMap.empty)
candidate -> (featureMap ++ preFilterPhase2FeatureMap)
}
previousPipelineResult.copy(
underlyingResult = previousPipelineResult.underlyingResult.copy(
preFilterHydrationResultPhase2 = Some(
executorResult
.asInstanceOf[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]])
),
featureMaps = Some(mergedFeatureMaps)
)
}
}
val FiltersStep =
new Step[(Query, Seq[CandidateWithFeatures[Result]]), FilterExecutorResult[Result]] {
override def identifier: PipelineStepIdentifier = CandidatePipelineConfig.filtersStep
override def executorArrow: Arrow[
(Query, Seq[CandidateWithFeatures[Result]]),
FilterExecutorResult[
Result
]
] =
filterExecutor.arrow(config.filters, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): (Query, Seq[CandidateWithFeatures[Result]]) = {
val candidates =
previousResult.underlyingResult.candidateSourceResult
.getOrElse {
throw InvalidStepStateException(identifier, "CandidateSourceResult")
}.candidates.map(_.candidate).asInstanceOf[Seq[Result]]
val featureMaps = previousResult.featureMaps
.getOrElse(throw InvalidStepStateException(identifier, "FeatureMaps"))
(
query.query,
candidates.map(candidate =>
CandidateWithFeaturesImpl(
candidate,
featureMaps.getOrElse(candidate, FeatureMap.empty))))
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: FilterExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(underlyingResult =
previousPipelineResult.underlyingResult.copy(
filterResult =
Some(executorResult.asInstanceOf[FilterExecutorResult[UniversalNoun[Any]]])
))
}
val PostFilterFeatureHydrationStep =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] {
override def identifier: PipelineStepIdentifier =
CandidatePipelineConfig.postFilterFeatureHydrationStep
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] =
candidateFeatureHydratorExecutor.arrow(config.postFilterFeatureHydration, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): CandidateFeatureHydratorExecutor.Inputs[Query, Result] = {
val filterResult = previousResult.underlyingResult.filterResult
.getOrElse(
throw InvalidStepStateException(identifier, "FilterResult")
).result.asInstanceOf[Seq[Result]]
val featureMaps = previousResult.featureMaps.getOrElse(
throw InvalidStepStateException(identifier, "FeatureMaps")
)
val filteredCandidates = filterResult.map { candidate =>
CandidateWithFeaturesImpl(candidate, featureMaps.getOrElse(candidate, FeatureMap.empty))
}
CandidateFeatureHydratorExecutor.Inputs(query.query, filteredCandidates)
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateFeatureHydratorExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] = {
val filterResult = previousPipelineResult.underlyingResult.filterResult
.getOrElse(
throw InvalidStepStateException(identifier, "FilterResult")
).result.asInstanceOf[Seq[Result]]
val featureMaps = previousPipelineResult.featureMaps.getOrElse(
throw InvalidStepStateException(identifier, "FeatureMaps")
)
val postFilterFeatureMaps = executorResult.results.map { result =>
result.candidate -> result.features
}.toMap
val mergedFeatureMaps = filterResult.map { candidate =>
candidate ->
(featureMaps
.getOrElse(candidate, FeatureMap.empty) ++ postFilterFeatureMaps.getOrElse(
candidate,
FeatureMap.empty))
}.toMap
previousPipelineResult.copy(
underlyingResult = previousPipelineResult.underlyingResult.copy(
postFilterHydrationResult = Some(
executorResult
.asInstanceOf[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]])
),
featureMaps = Some(mergedFeatureMaps)
)
}
}
val ScorersStep =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] {
override def identifier: PipelineStepIdentifier = CandidatePipelineConfig.scorersStep
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Result],
CandidateFeatureHydratorExecutorResult[Result]
] =
candidateFeatureHydratorExecutor.arrow(config.scorers, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): CandidateFeatureHydratorExecutor.Inputs[Query, Result] = {
val filterResult = previousResult.underlyingResult.filterResult
.getOrElse(
throw InvalidStepStateException(identifier, "FilterResult")
).result.asInstanceOf[Seq[Result]]
val featureMaps = previousResult.featureMaps.getOrElse(
throw InvalidStepStateException(identifier, "FeatureMaps")
)
val filteredCandidates = filterResult.map { candidate =>
CandidateWithFeaturesImpl(candidate, featureMaps.getOrElse(candidate, FeatureMap.empty))
}
CandidateFeatureHydratorExecutor.Inputs(query.query, filteredCandidates)
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateFeatureHydratorExecutorResult[Result]
): IntermediateCandidatePipelineResult[Result] = {
val filterResult = previousPipelineResult.underlyingResult.filterResult
.getOrElse(
throw InvalidStepStateException(identifier, "FilterResult")
).result.asInstanceOf[Seq[Result]]
val featureMaps = previousPipelineResult.featureMaps.getOrElse(
throw InvalidStepStateException(identifier, "FeatureMaps")
)
val scoringFeatureMaps = executorResult.results.map { result =>
result.candidate -> result.features
}.toMap
val mergedFeatureMaps = filterResult.map { candidate =>
candidate ->
(featureMaps
.getOrElse(candidate, FeatureMap.empty) ++ scoringFeatureMaps.getOrElse(
candidate,
FeatureMap.empty))
}.toMap
previousPipelineResult.copy(
underlyingResult = previousPipelineResult.underlyingResult.copy(
scorersResult = Some(
executorResult
.asInstanceOf[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]])
),
featureMaps = Some(mergedFeatureMaps)
)
}
}
val DecorationStep =
new Step[(Query, Seq[CandidateWithFeatures[Result]]), CandidateDecoratorExecutorResult] {
override def identifier: PipelineStepIdentifier = CandidatePipelineConfig.decoratorStep
override def executorArrow: Arrow[
(Query, Seq[CandidateWithFeatures[Result]]),
CandidateDecoratorExecutorResult
] =
candidateDecoratorExecutor.arrow(decorator, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): (Query, Seq[CandidateWithFeatures[Result]]) = {
val keptCandidates = previousResult.underlyingResult.filterResult
.getOrElse {
throw InvalidStepStateException(identifier, "FilterResult")
}.result.asInstanceOf[Seq[Result]]
val featureMaps = previousResult.featureMaps.getOrElse {
throw InvalidStepStateException(identifier, "FeatureMaps")
}
(
query.query,
keptCandidates.map(candidate =>
CandidateWithFeaturesImpl(
candidate,
featureMaps.getOrElse(candidate, FeatureMap.empty))))
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: CandidateDecoratorExecutorResult
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(underlyingResult =
previousPipelineResult.underlyingResult.copy(
candidateDecoratorResult = Some(executorResult)
))
}
/**
* ResultStep is a synchronous step that basically takes the outputs from the other steps, groups modules,
* and puts things into the final result object
*/
val ResultStep = new Step[GroupResultsExecutorInput[Result], GroupResultsExecutorResult] {
override def identifier: PipelineStepIdentifier = CandidatePipelineConfig.resultStep
override def executorArrow: Arrow[
GroupResultsExecutorInput[Result],
GroupResultsExecutorResult
] = groupResultsExecutor.arrow(pipelineIdentifier, candidateSourceIdentifier, context)
override def inputAdaptor(
query: CandidatePipeline.Inputs[Query],
previousResult: IntermediateCandidatePipelineResult[Result]
): GroupResultsExecutorInput[Result] = {
val underlying = previousResult.underlyingResult
val keptCandidates = underlying.filterResult
.getOrElse(
throw InvalidStepStateException(identifier, "FilterResult")
).result.asInstanceOf[Seq[Result]]
val decorations = underlying.candidateDecoratorResult
.getOrElse(
throw InvalidStepStateException(identifier, "DecorationResult")
).result.map(decoration => decoration.candidate -> decoration.presentation).toMap
val combinedFeatureMaps: Map[Result, FeatureMap] = previousResult.featureMaps.getOrElse(
throw InvalidStepStateException(identifier, "FeatureMaps"))
val filteredCandidates = keptCandidates.map { candidate =>
val updatedMap = combinedFeatureMaps
.get(candidate).getOrElse(FeatureMap.empty)
FetchedCandidateWithFeatures(candidate, updatedMap)
}
GroupResultsExecutorInput(
candidates = filteredCandidates,
decorations = decorations
)
}
override def resultUpdater(
previousPipelineResult: IntermediateCandidatePipelineResult[Result],
executorResult: GroupResultsExecutorResult
): IntermediateCandidatePipelineResult[Result] =
previousPipelineResult.copy(underlyingResult = previousPipelineResult.underlyingResult
.copy(result = Some(executorResult.candidatesWithDetails)))
}
val builtSteps = Seq(
GatesStep,
queryFeatureHydrationStep(
config.queryFeatureHydration,
CandidatePipelineConfig.fetchQueryFeaturesStep,
(pipelineResult, executorResult) =>
pipelineResult.copy(queryFeatures = Some(executorResult))
),
queryFeatureHydrationStep(
config.queryFeatureHydrationPhase2,
CandidatePipelineConfig.fetchQueryFeaturesPhase2Step,
(pipelineResult, executorResult) =>
pipelineResult.copy(
queryFeaturesPhase2 = Some(executorResult),
mergedAsyncQueryFeatures = Some(
pipelineResult.queryFeatures
.getOrElse(
throw InvalidStepStateException(
CandidatePipelineConfig.fetchQueryFeaturesPhase2Step,
"QueryFeatures")
).asyncFeatureMap ++ executorResult.asyncFeatureMap)
)
),
asyncFeaturesStep(CandidatePipelineConfig.candidateSourceStep, context),
CandidateSourceStep,
asyncFeaturesStep(CandidatePipelineConfig.preFilterFeatureHydrationPhase1Step, context),
PreFilterFeatureHydrationPhase1Step,
asyncFeaturesStep(CandidatePipelineConfig.preFilterFeatureHydrationPhase2Step, context),
PreFilterFeatureHydrationPhase2Step,
asyncFeaturesStep(CandidatePipelineConfig.filtersStep, context),
FiltersStep,
asyncFeaturesStep(CandidatePipelineConfig.postFilterFeatureHydrationStep, context),
PostFilterFeatureHydrationStep,
asyncFeaturesStep(CandidatePipelineConfig.scorersStep, context),
ScorersStep,
asyncFeaturesStep(CandidatePipelineConfig.decoratorStep, context),
DecorationStep,
ResultStep
)
/** The main execution logic for this Candidate Pipeline. */
val finalArrow: Arrow[CandidatePipeline.Inputs[Query], CandidatePipelineResult] =
buildCombinedArrowFromSteps(
steps = builtSteps,
context = context,
initialEmptyResult =
IntermediateCandidatePipelineResult.empty[Result](config.candidateSource.identifier),
stepsInOrderFromConfig = CandidatePipelineConfig.stepsInOrder
).map(_.underlyingResult)
val configFromBuilder = config
new CandidatePipeline[Query] {
override private[core] val config: BaseCandidatePipelineConfig[Query, _, _, _] =
configFromBuilder
override val arrow: Arrow[CandidatePipeline.Inputs[Query], CandidatePipelineResult] =
finalArrow
override val identifier: CandidatePipelineIdentifier = pipelineIdentifier
override val alerts: Seq[Alert] = config.alerts
override val children: Seq[Component] =
allGates ++
config.queryFeatureHydration ++
Seq(queryTransformer, config.candidateSource, resultsTransformer) ++
config.featuresFromCandidateSourceTransformers ++
decorator.toSeq ++
config.preFilterFeatureHydrationPhase1 ++
config.filters ++
config.postFilterFeatureHydration ++
config.scorers
}
}
private case class CandidateWithFeaturesImpl(candidate: Result, features: FeatureMap)
extends CandidateWithFeatures[Result]
}

View File

@ -1,56 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutor
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutor
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.candidate_source_executor.CandidateSourceExecutor
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.group_results_executor.GroupResultsExecutor
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CandidatePipelineBuilderFactory @Inject() (
queryFeatureHydratorExecutor: QueryFeatureHydratorExecutor,
asyncFeatureMapExecutor: AsyncFeatureMapExecutor,
candidateDecoratorExecutor: CandidateDecoratorExecutor,
candidateFeatureHydratorExecutor: CandidateFeatureHydratorExecutor,
candidateSourceExecutor: CandidateSourceExecutor,
groupResultsExecutor: GroupResultsExecutor,
filterExecutor: FilterExecutor,
gateExecutor: GateExecutor,
statsReceiver: StatsReceiver) {
def get[
Query <: PipelineQuery,
CandidateSourceQuery,
CandidateSourceResult,
Result <: UniversalNoun[Any]
]: CandidatePipelineBuilder[
Query,
CandidateSourceQuery,
CandidateSourceResult,
Result
] = {
new CandidatePipelineBuilder[
Query,
CandidateSourceQuery,
CandidateSourceResult,
Result
](
queryFeatureHydratorExecutor,
asyncFeatureMapExecutor,
candidateDecoratorExecutor,
candidateFeatureHydratorExecutor,
candidateSourceExecutor,
groupResultsExecutor,
filterExecutor,
gateExecutor,
statsReceiver
)
}
}

View File

@ -1,264 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.product_mixer.core.functional_component.candidate_source.BaseCandidateSource
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseCandidateFeatureHydrator
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseQueryFeatureHydrator
import com.twitter.product_mixer.core.functional_component.filter.Filter
import com.twitter.product_mixer.core.functional_component.gate.BaseGate
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.functional_component.scorer.Scorer
import com.twitter.product_mixer.core.functional_component.transformer.CandidateFeatureTransformer
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineQueryTransformer
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
import com.twitter.product_mixer.core.functional_component.transformer._
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.pipeline.PipelineConfig
import com.twitter.product_mixer.core.pipeline.PipelineConfigCompanion
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.decider.DeciderParam
sealed trait BaseCandidatePipelineConfig[
-Query <: PipelineQuery,
CandidateSourceQuery,
CandidateSourceResult,
Result <: UniversalNoun[Any]]
extends PipelineConfig {
val identifier: CandidatePipelineIdentifier
/**
* A candidate pipeline can fetch query-level features for use within the candidate source. It's
* generally recommended to set a hydrator in the parent recos or mixer pipeline if multiple
* candidate pipelines share the same feature but if a specific query feature hydrator is used
* by one pipeline and you don't want to block the others, you could explicitly set it here.
* If a feature is hydrated both in the parent pipeline or here, this one takes priority.
*/
def queryFeatureHydration: Seq[BaseQueryFeatureHydrator[Query, _]] = Seq.empty
/**
* For query-level features that are dependent on query-level features from [[queryFeatureHydration]]
*/
def queryFeatureHydrationPhase2: Seq[BaseQueryFeatureHydrator[Query, _]] = Seq.empty
/**
* When these Params are defined, they will automatically be added as Gates in the pipeline
* by the CandidatePipelineBuilder
*
* The enabled decider param can to be used to quickly disable a Candidate Pipeline via Decider
*/
val enabledDeciderParam: Option[DeciderParam[Boolean]] = None
/**
* This supported client feature switch param can be used with a Feature Switch to control the
* rollout of a new Candidate Pipeline from dogfood to experiment to production
*/
val supportedClientParam: Option[FSParam[Boolean]] = None
/** [[Gate]]s that are applied sequentially, the pipeline will only run if all the Gates are open */
def gates: Seq[BaseGate[Query]] = Seq.empty
/**
* A pair of transforms to adapt the underlying candidate source to the pipeline's query and result types
* Complex use cases such as those that need access to features should construct their own transformer, but
* for simple use cases, you can pass in an anonymous function.
* @example
* {{{ override val queryTransformer: CandidatePipelineQueryTransformer[Query, CandidateSourceQuery] = { query =>
* query.toExampleThrift
* }
* }}}
*/
def queryTransformer: BaseCandidatePipelineQueryTransformer[
Query,
CandidateSourceQuery
]
/** Source for Candidates for this Pipeline */
def candidateSource: BaseCandidateSource[CandidateSourceQuery, CandidateSourceResult]
/**
* [[CandidateFeatureTransformer]] allow you to define [[com.twitter.product_mixer.core.feature.Feature]] extraction logic from your [[CandidateSource]] results.
* If your candidate sources return [[com.twitter.product_mixer.core.feature.Feature]]s alongside the candidate that might be useful later on,
* add transformers for constructing feature maps.
*
* @note If multiple transformers extract the same feature, the last one takes priority and is kept.
*/
def featuresFromCandidateSourceTransformers: Seq[
CandidateFeatureTransformer[CandidateSourceResult]
] = Seq.empty
/**
* a result Transformer may throw PipelineFailure for candidates that are malformed and
* should be removed. This should be exceptional behavior, and not a replacement for adding a Filter.
* Complex use cases such as those that need access to features should construct their own transformer, but
* for simple use cases, you can pass in an anonymous function.
* @example
* {{{ override val queryTransformer: CandidatePipelineResultsTransformer[CandidateSourceResult, Result] = { sourceResult =>
* ExampleCandidate(sourceResult.id)
* }
* }}}
*
*/
val resultTransformer: CandidatePipelineResultsTransformer[CandidateSourceResult, Result]
/**
* Before filters are run, you can fetch features for each candidate.
*
* Uses Stitch, so you're encouraged to use a working Stitch Adaptor to batch between candidates.
*
* The existing features (from the candidate source) are passed in as an input. You are not expected
* to put them into the resulting feature map yourself - they will be merged for you by the platform.
*
* This API is likely to change when Product Mixer does managed feature hydration
*/
val preFilterFeatureHydrationPhase1: Seq[BaseCandidateFeatureHydrator[Query, Result, _]] =
Seq.empty
/**
* A second phase of feature hydration that can be run before filtering and after the first phase
* of [[preFilterFeatureHydrationPhase1]]. You are not expected to put them into the resulting
* feature map yourself - they will be merged for you by the platform.
*/
val preFilterFeatureHydrationPhase2: Seq[BaseCandidateFeatureHydrator[Query, Result, _]] =
Seq.empty
/** A list of filters to apply. Filters will be applied in sequential order. */
def filters: Seq[Filter[Query, Result]] = Seq.empty
/**
* After filters are run, you can fetch features for each candidate.
*
* Uses Stitch, so you're encouraged to use a working Stitch Adaptor to batch between candidates.
*
* The existing features (from the candidate source) & pre-filtering are passed in as an input.
* You are not expected to put them into the resulting feature map yourself -
* they will be merged for you by the platform.
*
* This API is likely to change when Product Mixer does managed feature hydration
*/
val postFilterFeatureHydration: Seq[BaseCandidateFeatureHydrator[Query, Result, _]] = Seq.empty
/**
* Decorators allow for adding Presentations to candidates. While the Presentation can contain any
* arbitrary data, Decorators are often used to add a UrtItemPresentation for URT item support, or
* a UrtModulePresentation for grouping the candidates in a URT module.
*/
val decorator: Option[CandidateDecorator[Query, Result]] = None
/**
* A candidate pipeline can define a partial function to rescue failures here. They will be treated as failures
* from a monitoring standpoint, and cancellation exceptions will always be propagated (they cannot be caught here).
*/
def failureClassifier: PartialFunction[Throwable, PipelineFailure] = PartialFunction.empty
/**
* Scorers for candidates. Scorers are executed in parallel. Order does not matter.
*/
def scorers: Seq[Scorer[Query, Result]] = Seq.empty
/**
* Alerts can be used to indicate the pipeline's service level objectives. Alerts and
* dashboards will be automatically created based on this information.
*/
val alerts: Seq[Alert] = Seq.empty
/**
* This method is used by the product mixer framework to build the pipeline.
*/
private[core] final def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
factory: CandidatePipelineBuilderFactory
): CandidatePipeline[Query] = {
factory.get.build(parentComponentIdentifierStack, this)
}
}
trait CandidatePipelineConfig[
-Query <: PipelineQuery,
CandidateSourceQuery,
CandidateSourceResult,
Result <: UniversalNoun[Any]]
extends BaseCandidatePipelineConfig[
Query,
CandidateSourceQuery,
CandidateSourceResult,
Result
] {
override val gates: Seq[Gate[Query]] = Seq.empty
override val queryTransformer: CandidatePipelineQueryTransformer[
Query,
CandidateSourceQuery
]
}
trait DependentCandidatePipelineConfig[
-Query <: PipelineQuery,
CandidateSourceQuery,
CandidateSourceResult,
Result <: UniversalNoun[Any]]
extends BaseCandidatePipelineConfig[
Query,
CandidateSourceQuery,
CandidateSourceResult,
Result
]
/**
* Contains [[PipelineStepIdentifier]]s for the Steps that are available for all [[BaseCandidatePipelineConfig]]s
*/
object CandidatePipelineConfig extends PipelineConfigCompanion {
val gatesStep: PipelineStepIdentifier = PipelineStepIdentifier("Gates")
val fetchQueryFeaturesStep: PipelineStepIdentifier = PipelineStepIdentifier("FetchQueryFeatures")
val fetchQueryFeaturesPhase2Step: PipelineStepIdentifier = PipelineStepIdentifier(
"FetchQueryFeaturesPhase2")
val candidateSourceStep: PipelineStepIdentifier = PipelineStepIdentifier("CandidateSource")
val preFilterFeatureHydrationPhase1Step: PipelineStepIdentifier =
PipelineStepIdentifier("PreFilterFeatureHydration")
val preFilterFeatureHydrationPhase2Step: PipelineStepIdentifier =
PipelineStepIdentifier("PreFilterFeatureHydrationPhase2")
val filtersStep: PipelineStepIdentifier = PipelineStepIdentifier("Filters")
val postFilterFeatureHydrationStep: PipelineStepIdentifier =
PipelineStepIdentifier("PostFilterFeatureHydration")
val scorersStep: PipelineStepIdentifier = PipelineStepIdentifier("Scorer")
val decoratorStep: PipelineStepIdentifier = PipelineStepIdentifier("Decorator")
val resultStep: PipelineStepIdentifier = PipelineStepIdentifier("Result")
/** All the steps which are executed by a [[CandidatePipeline]] in the order in which they are run */
override val stepsInOrder: Seq[PipelineStepIdentifier] = Seq(
gatesStep,
fetchQueryFeaturesStep,
fetchQueryFeaturesPhase2Step,
asyncFeaturesStep(candidateSourceStep),
candidateSourceStep,
asyncFeaturesStep(preFilterFeatureHydrationPhase1Step),
preFilterFeatureHydrationPhase1Step,
asyncFeaturesStep(preFilterFeatureHydrationPhase2Step),
preFilterFeatureHydrationPhase2Step,
asyncFeaturesStep(filtersStep),
filtersStep,
asyncFeaturesStep(postFilterFeatureHydrationStep),
postFilterFeatureHydrationStep,
asyncFeaturesStep(scorersStep),
scorersStep,
asyncFeaturesStep(decoratorStep),
decoratorStep,
resultStep
)
override val stepsAsyncFeatureHydrationCanBeCompletedBy: Set[PipelineStepIdentifier] = Set(
candidateSourceStep,
preFilterFeatureHydrationPhase1Step,
preFilterFeatureHydrationPhase2Step,
filtersStep,
postFilterFeatureHydrationStep,
scorersStep,
decoratorStep
)
}

View File

@ -1,93 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.pipeline.PipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutorResults
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_source_executor.CandidateSourceExecutorResult
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
case class CandidatePipelineResult(
candidateSourceIdentifier: CandidateSourceIdentifier,
gateResult: Option[GateExecutorResult],
queryFeatures: Option[QueryFeatureHydratorExecutor.Result],
queryFeaturesPhase2: Option[QueryFeatureHydratorExecutor.Result],
mergedAsyncQueryFeatures: Option[AsyncFeatureMap],
candidateSourceResult: Option[CandidateSourceExecutorResult[UniversalNoun[Any]]],
preFilterHydrationResult: Option[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]],
preFilterHydrationResultPhase2: Option[
CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]
],
filterResult: Option[FilterExecutorResult[UniversalNoun[Any]]],
postFilterHydrationResult: Option[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]],
candidateDecoratorResult: Option[CandidateDecoratorExecutorResult],
scorersResult: Option[CandidateFeatureHydratorExecutorResult[UniversalNoun[Any]]],
asyncFeatureHydrationResults: Option[AsyncFeatureMapExecutorResults],
failure: Option[PipelineFailure],
result: Option[Seq[CandidateWithDetails]])
extends PipelineResult[Seq[CandidateWithDetails]] {
override def withFailure(failure: PipelineFailure): CandidatePipelineResult =
copy(failure = Some(failure))
override def withResult(
result: Seq[CandidateWithDetails]
): CandidatePipelineResult = copy(result = Some(result))
override val resultSize: Int = result.map(PipelineResult.resultSize).getOrElse(0)
}
private[candidate] object IntermediateCandidatePipelineResult {
def empty[Candidate <: UniversalNoun[Any]](
candidateSourceIdentifier: CandidateSourceIdentifier
): IntermediateCandidatePipelineResult[Candidate] = {
IntermediateCandidatePipelineResult(
CandidatePipelineResult(
candidateSourceIdentifier = candidateSourceIdentifier,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None
),
None
)
}
}
private[candidate] case class IntermediateCandidatePipelineResult[Candidate <: UniversalNoun[Any]](
underlyingResult: CandidatePipelineResult,
featureMaps: Option[Map[Candidate, FeatureMap]])
extends PipelineResult[Seq[CandidateWithDetails]] {
override val failure: Option[PipelineFailure] = underlyingResult.failure
override val result: Option[Seq[CandidateWithDetails]] = underlyingResult.result
override def withFailure(
failure: PipelineFailure
): IntermediateCandidatePipelineResult[Candidate] =
copy(underlyingResult = underlyingResult.withFailure(failure))
override def withResult(
result: Seq[CandidateWithDetails]
): IntermediateCandidatePipelineResult[Candidate] =
copy(underlyingResult = underlyingResult.withResult(result))
override def resultSize(): Int = underlyingResult.resultSize
}

View File

@ -1,47 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.product_mixer.core.functional_component.candidate_source.PassthroughCandidateSource
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateExtractor
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineQueryTransformer
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
import com.twitter.product_mixer.core.pipeline.PipelineQuery
object PassthroughCandidatePipelineConfig {
/**
* Build a [[PassthroughCandidatePipelineConfig]] with a [[PassthroughCandidateSource]] built from
* a [[CandidateExtractor]]
*/
def apply[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]](
identifier: CandidatePipelineIdentifier,
extractor: CandidateExtractor[Query, Candidate],
decorator: Option[CandidateDecorator[Query, Candidate]] = None
): PassthroughCandidatePipelineConfig[Query, Candidate] = {
// Renaming variables to keep the interface clean, but avoid naming collisions when creating
// the anonymous class.
val _identifier = identifier
val _extractor = extractor
val _decorator = decorator
new PassthroughCandidatePipelineConfig[Query, Candidate] {
override val identifier = _identifier
override val candidateSource =
PassthroughCandidateSource(CandidateSourceIdentifier(_identifier.name), _extractor)
override val decorator = _decorator
}
}
}
trait PassthroughCandidatePipelineConfig[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]]
extends CandidatePipelineConfig[Query, Query, Candidate, Candidate] {
override val queryTransformer: CandidatePipelineQueryTransformer[Query, Query] = identity
override val resultTransformer: CandidatePipelineResultsTransformer[Candidate, Candidate] =
identity
}

View File

@ -1,51 +0,0 @@
package com.twitter.product_mixer.core.pipeline.candidate
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource
import com.twitter.product_mixer.core.functional_component.candidate_source.StaticCandidateSource
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineQueryTransformer
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
object StaticCandidatePipelineConfig {
/**
* Build a [[StaticCandidatePipelineConfig]] with a [[CandidateSource]] that returns the [[candidate]]
*/
def apply[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]](
identifier: CandidatePipelineIdentifier,
candidate: Candidate,
decorator: Option[CandidateDecorator[Query, Candidate]] = None
): StaticCandidatePipelineConfig[Query, Candidate] = {
// Renaming variables to keep the interface clean, but avoid naming collisions when creating
// the anonymous class.
val _identifier = identifier
val _candidate = candidate
val _decorator = decorator
new StaticCandidatePipelineConfig[Query, Candidate] {
override val identifier = _identifier
override val candidate = _candidate
override val decorator = _decorator
}
}
}
trait StaticCandidatePipelineConfig[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]]
extends CandidatePipelineConfig[Query, Unit, Unit, Candidate] {
val candidate: Candidate
override def candidateSource: CandidateSource[Unit, Unit] = StaticCandidateSource[Unit](
identifier = CandidateSourceIdentifier(identifier.name),
result = Seq(()))
override val queryTransformer: CandidatePipelineQueryTransformer[Query, Unit] = _ => Unit
override val resultTransformer: CandidatePipelineResultsTransformer[Unit, Candidate] = _ =>
candidate
}

View File

@ -1,62 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featurestorev1",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/filter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/premarshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/side_effect",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/async_feature_map_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/domain_marshaller_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_result_side_effect_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/selector_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/transport_marshaller_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/util",
"stitch/stitch-core",
],
exports = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/filter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/premarshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/side_effect",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/domain_marshaller_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_result_side_effect_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/selector_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/transport_marshaller_executor",
"stitch/stitch-core",
],
)

View File

@ -1,24 +0,0 @@
package com.twitter.product_mixer.core.pipeline.mixer
import com.twitter.product_mixer.core.model.common.identifier.MixerPipelineIdentifier
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.stitch.Arrow
/**
* A Mixer Pipeline
*
* This is an abstract class, as we only construct these via the [[MixerPipelineBuilder]].
*
* A [[MixerPipeline]] is capable of processing requests (queries) and returning responses (results)
* in the correct format to directly send to users.
*
* @tparam Query the domain model for the query or request
* @tparam Result the final marshalled result type
*/
abstract class MixerPipeline[Query <: PipelineQuery, Result] private[mixer]
extends Pipeline[Query, Result] {
override private[core] val config: MixerPipelineConfig[Query, _, Result]
override val arrow: Arrow[Query, MixerPipelineResult[Result]]
override val identifier: MixerPipelineIdentifier
}

View File

@ -1,582 +0,0 @@
package com.twitter.product_mixer.core.pipeline.mixer
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.feature_hydrator.QueryFeatureHydrator
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.functional_component.premarshaller.DomainMarshaller
import com.twitter.product_mixer.core.functional_component.selector.Selector
import com.twitter.product_mixer.core.functional_component.side_effect.PipelineResultSideEffect
import com.twitter.product_mixer.core.functional_component.marshaller.TransportMarshaller
import com.twitter.product_mixer.core.model.common.Component
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.MixerPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.FailOpenPolicy
import com.twitter.product_mixer.core.pipeline.InvalidStepStateException
import com.twitter.product_mixer.core.pipeline.PipelineBuilder
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipeline
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineBuilderFactory
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailureClassifier
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ProductDisabled
import com.twitter.product_mixer.core.quality_factor.HasQualityFactorStatus
import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver
import com.twitter.product_mixer.core.quality_factor.QualityFactorStatus
import com.twitter.product_mixer.core.service.Executor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutorResults
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutor
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutorResult
import com.twitter.product_mixer.core.service.domain_marshaller_executor.DomainMarshallerExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.StoppedGateException
import com.twitter.product_mixer.core.service.pipeline_result_side_effect_executor.PipelineResultSideEffectExecutor
import com.twitter.product_mixer.core.service.quality_factor_executor.QualityFactorExecutorResult
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
import com.twitter.product_mixer.core.service.transport_marshaller_executor.TransportMarshallerExecutor
import com.twitter.stitch.Arrow
import com.twitter.util.logging.Logging
/**
* MixerPipelineBuilder builds [[MixerPipeline]]s from [[MixerPipelineConfig]]s.
*
* You should inject a [[MixerPipelineBuilderFactory]] and call `.get` to build these.
*
* @see [[MixerPipelineConfig]] for the description of the type parameters
*/
class MixerPipelineBuilder[Query <: PipelineQuery, DomainResultType <: HasMarshalling, Result](
candidatePipelineExecutor: CandidatePipelineExecutor,
gateExecutor: GateExecutor,
selectorExecutor: SelectorExecutor,
queryFeatureHydratorExecutor: QueryFeatureHydratorExecutor,
asyncFeatureMapExecutor: AsyncFeatureMapExecutor,
domainMarshallerExecutor: DomainMarshallerExecutor,
transportMarshallerExecutor: TransportMarshallerExecutor,
pipelineResultSideEffectExecutor: PipelineResultSideEffectExecutor,
candidatePipelineBuilderFactory: CandidatePipelineBuilderFactory,
override val statsReceiver: StatsReceiver)
extends PipelineBuilder[Query]
with Logging {
override type UnderlyingResultType = Result
override type PipelineResultType = MixerPipelineResult[Result]
def qualityFactorStep(
qualityFactorStatus: QualityFactorStatus
): Step[Query, QualityFactorExecutorResult] =
new Step[Query, QualityFactorExecutorResult] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.qualityFactorStep
override def executorArrow: Arrow[Query, QualityFactorExecutorResult] =
Arrow
.map[Query, QualityFactorExecutorResult] { _ =>
QualityFactorExecutorResult(
pipelineQualityFactors =
qualityFactorStatus.qualityFactorByPipeline.mapValues(_.currentValue)
)
}
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): Query = query
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: QualityFactorExecutorResult
): MixerPipelineResult[Result] =
previousPipelineResult.copy(qualityFactorResult = Some(executorResult))
override def queryUpdater(
query: Query,
executorResult: QualityFactorExecutorResult
): Query = {
query match {
case queryWithQualityFactor: HasQualityFactorStatus =>
queryWithQualityFactor
.withQualityFactorStatus(
queryWithQualityFactor.qualityFactorStatus.getOrElse(QualityFactorStatus.empty) ++
qualityFactorStatus
).asInstanceOf[Query]
case _ =>
query
}
}
}
def gatesStep(
gates: Seq[Gate[Query]],
context: Executor.Context
): Step[Query, GateExecutorResult] = new Step[Query, GateExecutorResult] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.gatesStep
override def executorArrow: Arrow[Query, GateExecutorResult] =
gateExecutor.arrow(gates, context)
override def inputAdaptor(query: Query, previousResult: MixerPipelineResult[Result]): Query =
query
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: GateExecutorResult
): MixerPipelineResult[Result] =
previousPipelineResult.copy(gateResult = Some(executorResult))
}
def fetchQueryFeaturesStep(
queryFeatureHydrators: Seq[QueryFeatureHydrator[Query]],
stepIdentifier: PipelineStepIdentifier,
updater: ResultUpdater[MixerPipelineResult[Result], QueryFeatureHydratorExecutor.Result],
context: Executor.Context
): Step[Query, QueryFeatureHydratorExecutor.Result] =
new Step[Query, QueryFeatureHydratorExecutor.Result] {
override def identifier: PipelineStepIdentifier = stepIdentifier
override def executorArrow: Arrow[Query, QueryFeatureHydratorExecutor.Result] =
queryFeatureHydratorExecutor.arrow(
queryFeatureHydrators,
MixerPipelineConfig.stepsAsyncFeatureHydrationCanBeCompletedBy,
context)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): Query = query
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: QueryFeatureHydratorExecutor.Result
): MixerPipelineResult[Result] =
updater(previousPipelineResult, executorResult)
override def queryUpdater(
query: Query,
executorResult: QueryFeatureHydratorExecutor.Result
): Query =
query
.withFeatureMap(
query.features
.getOrElse(FeatureMap.empty) ++ executorResult.featureMap).asInstanceOf[Query]
}
def asyncFeaturesStep(
stepToHydrateFor: PipelineStepIdentifier,
context: Executor.Context
): Step[AsyncFeatureMap, AsyncFeatureMapExecutorResults] =
new Step[AsyncFeatureMap, AsyncFeatureMapExecutorResults] {
override def identifier: PipelineStepIdentifier =
MixerPipelineConfig.asyncFeaturesStep(stepToHydrateFor)
override def executorArrow: Arrow[AsyncFeatureMap, AsyncFeatureMapExecutorResults] =
asyncFeatureMapExecutor.arrow(
stepToHydrateFor,
identifier,
context
)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): AsyncFeatureMap =
previousResult.mergedAsyncQueryFeatures
.getOrElse(
throw InvalidStepStateException(identifier, "MergedAsyncQueryFeatures")
)
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: AsyncFeatureMapExecutorResults
): MixerPipelineResult[Result] = previousPipelineResult.copy(
asyncFeatureHydrationResults = previousPipelineResult.asyncFeatureHydrationResults match {
case Some(existingResults) => Some(existingResults ++ executorResult)
case None => Some(executorResult)
})
override def queryUpdater(
query: Query,
executorResult: AsyncFeatureMapExecutorResults
): Query =
if (executorResult.featureMapsByStep
.getOrElse(stepToHydrateFor, FeatureMap.empty).isEmpty) {
query
} else {
query
.withFeatureMap(
query.features
.getOrElse(FeatureMap.empty) ++ executorResult.featureMapsByStep(
stepToHydrateFor)).asInstanceOf[Query]
}
}
def candidatePipelinesStep(
candidatePipelines: Seq[CandidatePipeline[Query]],
defaultFailOpenPolicy: FailOpenPolicy,
failOpenPolicies: Map[CandidatePipelineIdentifier, FailOpenPolicy],
qualityFactorObserverByPipeline: Map[ComponentIdentifier, QualityFactorObserver],
context: Executor.Context
): Step[CandidatePipeline.Inputs[Query], CandidatePipelineExecutorResult] =
new Step[CandidatePipeline.Inputs[Query], CandidatePipelineExecutorResult] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.candidatePipelinesStep
override def executorArrow: Arrow[CandidatePipeline.Inputs[
Query
], CandidatePipelineExecutorResult] =
candidatePipelineExecutor
.arrow(
candidatePipelines,
defaultFailOpenPolicy,
failOpenPolicies,
qualityFactorObserverByPipeline,
context
)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): CandidatePipeline.Inputs[Query] = CandidatePipeline.Inputs[Query](query, Seq.empty)
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: CandidatePipelineExecutorResult
): MixerPipelineResult[Result] =
previousPipelineResult.copy(candidatePipelineResults = Some(executorResult))
override def queryUpdater(
query: Query,
executorResult: CandidatePipelineExecutorResult
): Query = {
val updatedFeatureMap = query.features
.getOrElse(FeatureMap.empty) ++ executorResult.queryFeatureMap
query
.withFeatureMap(updatedFeatureMap).asInstanceOf[Query]
}
}
def dependentCandidatePipelinesStep(
candidatePipelines: Seq[CandidatePipeline[Query]],
defaultFailOpenPolicy: FailOpenPolicy,
failOpenPolicies: Map[CandidatePipelineIdentifier, FailOpenPolicy],
qualityFactorObserverByPipeline: Map[ComponentIdentifier, QualityFactorObserver],
context: Executor.Context
): Step[CandidatePipeline.Inputs[Query], CandidatePipelineExecutorResult] =
new Step[CandidatePipeline.Inputs[Query], CandidatePipelineExecutorResult] {
override def identifier: PipelineStepIdentifier =
MixerPipelineConfig.dependentCandidatePipelinesStep
override def executorArrow: Arrow[CandidatePipeline.Inputs[
Query
], CandidatePipelineExecutorResult] =
candidatePipelineExecutor
.arrow(
candidatePipelines,
defaultFailOpenPolicy,
failOpenPolicies,
qualityFactorObserverByPipeline,
context
)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): CandidatePipeline.Inputs[Query] = {
val previousCandidates = previousResult.candidatePipelineResults
.getOrElse {
throw InvalidStepStateException(identifier, "Candidates")
}.candidatePipelineResults.flatMap(_.result.getOrElse(Seq.empty))
CandidatePipeline.Inputs[Query](query, previousCandidates)
}
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: CandidatePipelineExecutorResult
): MixerPipelineResult[Result] =
previousPipelineResult.copy(dependentCandidatePipelineResults = Some(executorResult))
override def queryUpdater(
query: Query,
executorResult: CandidatePipelineExecutorResult
): Query = {
val updatedFeatureMap = query.features
.getOrElse(FeatureMap.empty) ++ executorResult.queryFeatureMap
query
.withFeatureMap(updatedFeatureMap).asInstanceOf[Query]
}
}
def resultSelectorsStep(
selectors: Seq[Selector[Query]],
context: Executor.Context
): Step[SelectorExecutor.Inputs[Query], SelectorExecutorResult] =
new Step[SelectorExecutor.Inputs[Query], SelectorExecutorResult] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.resultSelectorsStep
override def executorArrow: Arrow[SelectorExecutor.Inputs[Query], SelectorExecutorResult] =
selectorExecutor.arrow(selectors, context)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): SelectorExecutor.Inputs[Query] = {
val candidates = previousResult.candidatePipelineResults
.getOrElse {
throw InvalidStepStateException(identifier, "Candidates")
}.candidatePipelineResults.flatMap(_.result.getOrElse(Seq.empty))
val dependentCandidates =
previousResult.dependentCandidatePipelineResults
.getOrElse {
throw InvalidStepStateException(identifier, "DependentCandidates")
}.candidatePipelineResults.flatMap(_.result.getOrElse(Seq.empty))
SelectorExecutor.Inputs(
query = query,
candidatesWithDetails = candidates ++ dependentCandidates
)
}
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: SelectorExecutorResult
): MixerPipelineResult[Result] =
previousPipelineResult.copy(resultSelectorResults = Some(executorResult))
}
def domainMarshallingStep(
domainMarshaller: DomainMarshaller[Query, DomainResultType],
context: Executor.Context
): Step[DomainMarshallerExecutor.Inputs[Query], DomainMarshallerExecutor.Result[
DomainResultType
]] =
new Step[DomainMarshallerExecutor.Inputs[Query], DomainMarshallerExecutor.Result[
DomainResultType
]] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.domainMarshallerStep
override def executorArrow: Arrow[
DomainMarshallerExecutor.Inputs[Query],
DomainMarshallerExecutor.Result[DomainResultType]
] =
domainMarshallerExecutor.arrow(domainMarshaller, context)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): DomainMarshallerExecutor.Inputs[Query] = {
val selectorResults = previousResult.resultSelectorResults.getOrElse {
throw InvalidStepStateException(identifier, "SelectorResults")
}
DomainMarshallerExecutor.Inputs(
query = query,
candidatesWithDetails = selectorResults.selectedCandidates
)
}
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: DomainMarshallerExecutor.Result[DomainResultType]
): MixerPipelineResult[Result] = previousPipelineResult.copy(
domainMarshallerResults = Some(executorResult)
)
}
def resultSideEffectsStep(
sideEffects: Seq[PipelineResultSideEffect[Query, DomainResultType]],
context: Executor.Context
): Step[
PipelineResultSideEffect.Inputs[Query, DomainResultType],
PipelineResultSideEffectExecutor.Result
] = new Step[
PipelineResultSideEffect.Inputs[Query, DomainResultType],
PipelineResultSideEffectExecutor.Result
] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.resultSideEffectsStep
override def executorArrow: Arrow[
PipelineResultSideEffect.Inputs[Query, DomainResultType],
PipelineResultSideEffectExecutor.Result
] = pipelineResultSideEffectExecutor.arrow(sideEffects, context)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): PipelineResultSideEffect.Inputs[Query, DomainResultType] = {
val selectorResults = previousResult.resultSelectorResults.getOrElse {
throw InvalidStepStateException(identifier, "SelectorResults")
}
val domainMarshallerResults = previousResult.domainMarshallerResults.getOrElse {
throw InvalidStepStateException(identifier, "DomainMarshallerResults")
}
PipelineResultSideEffect.Inputs[Query, DomainResultType](
query = query,
selectedCandidates = selectorResults.selectedCandidates,
remainingCandidates = selectorResults.remainingCandidates,
droppedCandidates = selectorResults.droppedCandidates,
response = domainMarshallerResults.result.asInstanceOf[DomainResultType]
)
}
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: PipelineResultSideEffectExecutor.Result
): MixerPipelineResult[Result] =
previousPipelineResult.copy(resultSideEffectResults = Some(executorResult))
}
def transportMarshallingStep(
transportMarshaller: TransportMarshaller[DomainResultType, Result],
context: Executor.Context
): Step[
TransportMarshallerExecutor.Inputs[DomainResultType],
TransportMarshallerExecutor.Result[Result]
] = new Step[TransportMarshallerExecutor.Inputs[
DomainResultType
], TransportMarshallerExecutor.Result[Result]] {
override def identifier: PipelineStepIdentifier = MixerPipelineConfig.transportMarshallerStep
override def executorArrow: Arrow[TransportMarshallerExecutor.Inputs[
DomainResultType
], TransportMarshallerExecutor.Result[Result]] =
transportMarshallerExecutor.arrow(transportMarshaller, context)
override def inputAdaptor(
query: Query,
previousResult: MixerPipelineResult[Result]
): TransportMarshallerExecutor.Inputs[DomainResultType] = {
val domainMarshallingResults = previousResult.domainMarshallerResults.getOrElse {
throw InvalidStepStateException(identifier, "DomainMarshallerResults")
}
// Since the PipelineResult just uses HasMarshalling
val domainResult = domainMarshallingResults.result.asInstanceOf[DomainResultType]
TransportMarshallerExecutor.Inputs(domainResult)
}
override def resultUpdater(
previousPipelineResult: MixerPipelineResult[Result],
executorResult: TransportMarshallerExecutor.Result[Result]
): MixerPipelineResult[Result] = previousPipelineResult.copy(
transportMarshallerResults = Some(executorResult),
result = Some(executorResult.result)
)
}
def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
config: MixerPipelineConfig[Query, DomainResultType, Result]
): MixerPipeline[Query, Result] = {
val pipelineIdentifier = config.identifier
val context = Executor.Context(
PipelineFailureClassifier(
config.failureClassifier.orElse(StoppedGateException.classifier(ProductDisabled))),
parentComponentIdentifierStack.push(pipelineIdentifier)
)
val qualityFactorStatus: QualityFactorStatus =
QualityFactorStatus.build(config.qualityFactorConfigs)
val qualityFactorObserverByPipeline =
qualityFactorStatus.qualityFactorByPipeline.mapValues { qualityFactor =>
qualityFactor.buildObserver()
}
buildGaugesForQualityFactor(pipelineIdentifier, qualityFactorStatus, statsReceiver)
val candidatePipelines: Seq[CandidatePipeline[Query]] = config.candidatePipelines.map {
pipelineConfig: CandidatePipelineConfig[Query, _, _, _] =>
pipelineConfig.build(context.componentStack, candidatePipelineBuilderFactory)
}
val dependentCandidatePipelines: Seq[CandidatePipeline[Query]] =
config.dependentCandidatePipelines.map {
pipelineConfig: DependentCandidatePipelineConfig[Query, _, _, _] =>
pipelineConfig.build(context.componentStack, candidatePipelineBuilderFactory)
}
val builtSteps = Seq(
qualityFactorStep(qualityFactorStatus),
gatesStep(config.gates, context),
fetchQueryFeaturesStep(
config.fetchQueryFeatures,
MixerPipelineConfig.fetchQueryFeaturesStep,
(previousPipelineResult, executorResult) =>
previousPipelineResult.copy(queryFeatures = Some(executorResult)),
context
),
fetchQueryFeaturesStep(
config.fetchQueryFeaturesPhase2,
MixerPipelineConfig.fetchQueryFeaturesPhase2Step,
(previousPipelineResult, executorResult) =>
previousPipelineResult.copy(
queryFeaturesPhase2 = Some(executorResult),
mergedAsyncQueryFeatures = Some(
previousPipelineResult.queryFeatures
.getOrElse(throw InvalidStepStateException(
MixerPipelineConfig.fetchQueryFeaturesPhase2Step,
"QueryFeatures"))
.asyncFeatureMap ++ executorResult.asyncFeatureMap)
),
context
),
asyncFeaturesStep(MixerPipelineConfig.candidatePipelinesStep, context),
candidatePipelinesStep(
candidatePipelines,
config.defaultFailOpenPolicy,
config.failOpenPolicies,
qualityFactorObserverByPipeline,
context),
asyncFeaturesStep(MixerPipelineConfig.dependentCandidatePipelinesStep, context),
dependentCandidatePipelinesStep(
dependentCandidatePipelines,
config.defaultFailOpenPolicy,
config.failOpenPolicies,
qualityFactorObserverByPipeline,
context),
asyncFeaturesStep(MixerPipelineConfig.resultSelectorsStep, context),
resultSelectorsStep(config.resultSelectors, context),
domainMarshallingStep(config.domainMarshaller, context),
asyncFeaturesStep(MixerPipelineConfig.resultSideEffectsStep, context),
resultSideEffectsStep(config.resultSideEffects, context),
transportMarshallingStep(config.transportMarshaller, context)
)
val finalArrow = buildCombinedArrowFromSteps(
steps = builtSteps,
context = context,
initialEmptyResult = MixerPipelineResult.empty,
stepsInOrderFromConfig = MixerPipelineConfig.stepsInOrder
)
val configFromBuilder = config
new MixerPipeline[Query, Result] {
override private[core] val config: MixerPipelineConfig[Query, _, Result] = configFromBuilder
override val arrow: Arrow[Query, MixerPipelineResult[Result]] = finalArrow
override val identifier: MixerPipelineIdentifier = pipelineIdentifier
override val alerts: Seq[Alert] = config.alerts
override val children: Seq[Component] =
config.gates ++
config.fetchQueryFeatures ++
candidatePipelines ++
dependentCandidatePipelines ++
config.resultSideEffects ++
Seq(config.domainMarshaller, config.transportMarshaller)
}
}
}

View File

@ -1,49 +0,0 @@
package com.twitter.product_mixer.core.pipeline.mixer
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineBuilderFactory
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutor
import com.twitter.product_mixer.core.service.domain_marshaller_executor.DomainMarshallerExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.pipeline_result_side_effect_executor.PipelineResultSideEffectExecutor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutor
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutor
import com.twitter.product_mixer.core.service.transport_marshaller_executor.TransportMarshallerExecutor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MixerPipelineBuilderFactory @Inject() (
candidatePipelineExecutor: CandidatePipelineExecutor,
gateExecutor: GateExecutor,
selectorExecutor: SelectorExecutor,
queryFeatureHydratorExecutor: QueryFeatureHydratorExecutor,
asyncFeatureMapExecutor: AsyncFeatureMapExecutor,
domainMarshallerExecutor: DomainMarshallerExecutor,
transportMarshallerExecutor: TransportMarshallerExecutor,
pipelineResultSideEffectExecutor: PipelineResultSideEffectExecutor,
candidatePipelineBuilderFactory: CandidatePipelineBuilderFactory,
statsReceiver: StatsReceiver) {
def get[
Query <: PipelineQuery,
DomainResultType <: HasMarshalling,
Result
]: MixerPipelineBuilder[Query, DomainResultType, Result] = {
new MixerPipelineBuilder[Query, DomainResultType, Result](
candidatePipelineExecutor,
gateExecutor,
selectorExecutor,
queryFeatureHydratorExecutor,
asyncFeatureMapExecutor,
domainMarshallerExecutor,
transportMarshallerExecutor,
pipelineResultSideEffectExecutor,
candidatePipelineBuilderFactory,
statsReceiver
)
}
}

View File

@ -1,175 +0,0 @@
package com.twitter.product_mixer.core.pipeline.mixer
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.feature_hydrator.QueryFeatureHydrator
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.functional_component.premarshaller.DomainMarshaller
import com.twitter.product_mixer.core.functional_component.selector.Selector
import com.twitter.product_mixer.core.functional_component.side_effect.PipelineResultSideEffect
import com.twitter.product_mixer.core.functional_component.marshaller.TransportMarshaller
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.MixerPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.FailOpenPolicy
import com.twitter.product_mixer.core.pipeline.PipelineConfig
import com.twitter.product_mixer.core.pipeline.PipelineConfigCompanion
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ClosedGate
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.quality_factor.QualityFactorConfig
/**
* This is the configuration necessary to generate a Mixer Pipeline. Product code should create a
* MixerPipelineConfig, and then use a MixerPipelineBuilder to get the final MixerPipeline which can
* process requests.
*
* @tparam Query - The domain model for the query or request
* @tparam UnmarshalledResultType - The result type of the pipeline, but before marshalling to a wire protocol like URT
* @tparam Result - The final result that will be served to users
*/
trait MixerPipelineConfig[Query <: PipelineQuery, UnmarshalledResultType <: HasMarshalling, Result]
extends PipelineConfig {
override val identifier: MixerPipelineIdentifier
/**
* Mixer Pipeline Gates will be executed before any other step (including retrieval from candidate
* pipelines). They're executed sequentially, and any "Stop" result will prevent pipeline execution.
*/
def gates: Seq[Gate[Query]] = Seq.empty
/**
* A mixer pipeline can fetch query-level features before candidate pipelines are executed.
*/
def fetchQueryFeatures: Seq[QueryFeatureHydrator[Query]] = Seq.empty
/**
* For query-level features that are dependent on query-level features from [[fetchQueryFeatures]]
*/
def fetchQueryFeaturesPhase2: Seq[QueryFeatureHydrator[Query]] = Seq.empty
/**
* Candidate pipelines retrieve candidates for possible inclusion in the result
*/
def candidatePipelines: Seq[CandidatePipelineConfig[Query, _, _, _]]
/**
* Dependent candidate pipelines to retrieve candidates that depend on the result of [[candidatePipelines]]
* [[DependentCandidatePipelineConfig]] have access to the list of previously retrieved & decorated
* candidates for use in constructing the query object.
*/
def dependentCandidatePipelines: Seq[DependentCandidatePipelineConfig[Query, _, _, _]] = Seq.empty
/**
* [[defaultFailOpenPolicy]] is the [[FailOpenPolicy]] that will be applied to any candidate
* pipeline that isn't in the [[failOpenPolicies]] map. By default Candidate Pipelines will fail
* open for Closed Gates only.
*/
def defaultFailOpenPolicy: FailOpenPolicy = FailOpenPolicy(Set(ClosedGate))
/**
* [[failOpenPolicies]] associates [[FailOpenPolicy]]s to specific candidate pipelines using
* [[CandidatePipelineIdentifier]].
*
* @note these [[FailOpenPolicy]]s override the [[defaultFailOpenPolicy]] for a mapped
* Candidate Pipeline.
*/
def failOpenPolicies: Map[CandidatePipelineIdentifier, FailOpenPolicy] = Map.empty
/**
** [[qualityFactorConfigs]] associates [[QualityFactorConfig]]s to specific candidate pipelines
* using [[CandidatePipelineIdentifier]].
*/
def qualityFactorConfigs: Map[CandidatePipelineIdentifier, QualityFactorConfig] =
Map.empty
/**
* Selectors are executed in sequential order to combine the candidates into a result
*/
def resultSelectors: Seq[Selector[Query]]
/**
* Mixer result side effects that are executed after selection and domain marshalling
*/
def resultSideEffects: Seq[PipelineResultSideEffect[Query, UnmarshalledResultType]] = Seq()
/**
* Domain marshaller transforms the selections into the model expected by the marshaller
*/
def domainMarshaller: DomainMarshaller[Query, UnmarshalledResultType]
/**
* Transport marshaller transforms the model into our line-level API like URT or JSON
*/
def transportMarshaller: TransportMarshaller[UnmarshalledResultType, Result]
/**
* A pipeline can define a partial function to rescue failures here. They will be treated as failures
* from a monitoring standpoint, and cancellation exceptions will always be propagated (they cannot be caught here).
*/
def failureClassifier: PartialFunction[Throwable, PipelineFailure] = PartialFunction.empty
/**
* Alert can be used to indicate the pipeline's service level objectives. Alerts and
* dashboards will be automatically created based on this information.
*/
val alerts: Seq[Alert] = Seq.empty
/**
* This method is used by the product mixer framework to build the pipeline.
*/
private[core] final def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
builder: MixerPipelineBuilderFactory
): MixerPipeline[Query, Result] =
builder.get.build(parentComponentIdentifierStack, this)
}
object MixerPipelineConfig extends PipelineConfigCompanion {
val qualityFactorStep: PipelineStepIdentifier = PipelineStepIdentifier("QualityFactor")
val gatesStep: PipelineStepIdentifier = PipelineStepIdentifier("Gates")
val fetchQueryFeaturesStep: PipelineStepIdentifier = PipelineStepIdentifier("FetchQueryFeatures")
val fetchQueryFeaturesPhase2Step: PipelineStepIdentifier =
PipelineStepIdentifier("FetchQueryFeaturesPhase2")
val candidatePipelinesStep: PipelineStepIdentifier = PipelineStepIdentifier("CandidatePipelines")
val dependentCandidatePipelinesStep: PipelineStepIdentifier =
PipelineStepIdentifier("DependentCandidatePipelines")
val resultSelectorsStep: PipelineStepIdentifier = PipelineStepIdentifier("ResultSelectors")
val domainMarshallerStep: PipelineStepIdentifier = PipelineStepIdentifier("DomainMarshaller")
val resultSideEffectsStep: PipelineStepIdentifier = PipelineStepIdentifier("ResultSideEffects")
val transportMarshallerStep: PipelineStepIdentifier = PipelineStepIdentifier(
"TransportMarshaller")
/** All the Steps which are executed by a [[MixerPipeline]] in the order in which they are run */
override val stepsInOrder: Seq[PipelineStepIdentifier] = Seq(
qualityFactorStep,
gatesStep,
fetchQueryFeaturesStep,
fetchQueryFeaturesPhase2Step,
asyncFeaturesStep(candidatePipelinesStep),
candidatePipelinesStep,
asyncFeaturesStep(dependentCandidatePipelinesStep),
dependentCandidatePipelinesStep,
asyncFeaturesStep(resultSelectorsStep),
resultSelectorsStep,
domainMarshallerStep,
asyncFeaturesStep(resultSideEffectsStep),
resultSideEffectsStep,
transportMarshallerStep
)
/**
* All the Steps which an [[com.twitter.product_mixer.core.functional_component.feature_hydrator.AsyncHydrator AsyncHydrator]]
* can be configured to [[com.twitter.product_mixer.core.functional_component.feature_hydrator.AsyncHydrator.hydrateBefore hydrateBefore]]
*/
override val stepsAsyncFeatureHydrationCanBeCompletedBy: Set[PipelineStepIdentifier] = Set(
candidatePipelinesStep,
dependentCandidatePipelinesStep,
resultSelectorsStep,
resultSideEffectsStep
)
}

View File

@ -1,70 +0,0 @@
package com.twitter.product_mixer.core.pipeline.mixer
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.PipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutorResults
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutorResult
import com.twitter.product_mixer.core.service.domain_marshaller_executor.DomainMarshallerExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.pipeline_result_side_effect_executor.PipelineResultSideEffectExecutor
import com.twitter.product_mixer.core.service.quality_factor_executor.QualityFactorExecutorResult
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
import com.twitter.product_mixer.core.service.transport_marshaller_executor.TransportMarshallerExecutor
/**
* A [[MixerPipelineResult]] includes both the user-visible [[PipelineResult]] and all the
* Execution details possible - intermediate results, what components did, etc.
*/
case class MixerPipelineResult[Result](
qualityFactorResult: Option[QualityFactorExecutorResult],
gateResult: Option[GateExecutorResult],
queryFeatures: Option[QueryFeatureHydratorExecutor.Result],
queryFeaturesPhase2: Option[QueryFeatureHydratorExecutor.Result],
mergedAsyncQueryFeatures: Option[AsyncFeatureMap],
candidatePipelineResults: Option[CandidatePipelineExecutorResult],
dependentCandidatePipelineResults: Option[CandidatePipelineExecutorResult],
resultSelectorResults: Option[SelectorExecutorResult],
domainMarshallerResults: Option[DomainMarshallerExecutor.Result[HasMarshalling]],
resultSideEffectResults: Option[PipelineResultSideEffectExecutor.Result],
asyncFeatureHydrationResults: Option[AsyncFeatureMapExecutorResults],
transportMarshallerResults: Option[TransportMarshallerExecutor.Result[Result]],
failure: Option[PipelineFailure],
result: Option[Result])
extends PipelineResult[Result] {
override def withFailure(failure: PipelineFailure): PipelineResult[Result] =
copy(failure = Some(failure))
override def withResult(result: Result): PipelineResult[Result] = copy(result = Some(result))
/**
* resultSize is calculated based on the selector results rather than the marshalled results. The
* structure of the marshalled format is unknown, making operating on selector results more
* convenient. This will implicitly excluded cursors built during marshalling but cursors don't
* contribute to the result size anyway.
*/
override val resultSize: Int =
resultSelectorResults.map(_.selectedCandidates).map(PipelineResult.resultSize).getOrElse(0)
}
object MixerPipelineResult {
def empty[A]: MixerPipelineResult[A] = MixerPipelineResult(
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None
)
}

View File

@ -1,14 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier",
"util/util-jackson/src/main/scala/com/twitter/util/jackson",
],
exports = [
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier",
],
)

View File

@ -1,49 +0,0 @@
package com.twitter.product_mixer.core.pipeline.pipeline_failure
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import scala.util.control.NoStackTrace
/**
* Pipeline Failures represent pipeline requests that were not able to complete.
*
* A pipeline result will always define either a result or a failure.
*
* The reason field should not be displayed to end-users, and is free to change over time.
* It should always be free of private user data such that we can log it.
*
* The pipeline can classify it's own failures into categories (timeouts, invalid arguments,
* rate limited, etc) such that the caller can choose how to handle it.
*
* @note [[componentStack]] should only be set by the product mixer framework,
* it should **NOT** be set when making a [[PipelineFailure]]
*/
@JsonSerialize(using = classOf[PipelineFailureSerializer])
case class PipelineFailure(
category: PipelineFailureCategory,
reason: String,
underlying: Option[Throwable] = None,
componentStack: Option[ComponentIdentifierStack] = None)
extends Exception(
"PipelineFailure(" +
s"category = $category, " +
s"reason = $reason, " +
s"underlying = $underlying, " +
s"componentStack = $componentStack)",
underlying.orNull
) {
override def toString: String = getMessage
/** Returns an updated copy of this [[PipelineFailure]] with the same exception stacktrace */
def copy(
category: PipelineFailureCategory = this.category,
reason: String = this.reason,
underlying: Option[Throwable] = this.underlying,
componentStack: Option[ComponentIdentifierStack] = this.componentStack
): PipelineFailure = {
val newPipelineFailure =
new PipelineFailure(category, reason, underlying, componentStack) with NoStackTrace
newPipelineFailure.setStackTrace(this.getStackTrace)
newPipelineFailure
}
}

View File

@ -1,190 +0,0 @@
package com.twitter.product_mixer.core.pipeline.pipeline_failure
/**
* Failures are grouped into categories based on which party is 'responsible' for the issue. This
* is important for generating accurate SLOs and ensuring that the correct team is alerted.
*/
sealed trait PipelineFailureCategory {
val categoryName: String
val failureName: String
}
/**
* Client Failures are failures where the client is deemed responsible for the issue. Such as by
* issuing an invalid request or not having the right permissions.
*
* A failure might belong in this category if it relates to behaviour on the client and is not
* actionable by the team which owns the product.
*/
trait ClientFailure extends PipelineFailureCategory {
override val categoryName: String = "ClientFailure"
}
/**
* The requested product is disabled so the request cannot be served.
*/
case object ProductDisabled extends ClientFailure {
override val failureName: String = "ProductDisabled"
}
/**
* The request was deemed invalid by this or a backing service.
*/
case object BadRequest extends ClientFailure {
override val failureName: String = "BadRequest"
}
/**
* Credentials proving the identity of the caller were missing, not trusted, or expired.
* For example, an auth cookie might be expired and in need of refreshing.
*
* Do not confuse this with Authorization, where the credentials are believed but not allowed to perform the operation.
*/
case object Authentication extends ClientFailure {
override val failureName: String = "Authentication"
}
/**
* The operation was forbidden (often, but not always, by a Strato access control policy).
*
* Do not confuse this with Authentication, where the given credentials were missing, not trusted, or expired.
*/
case object Unauthorized extends ClientFailure {
override val failureName: String = "Unauthorized"
}
/**
* The operation returned a Not Found response.
*/
case object NotFound extends ClientFailure {
override val failureName: String = "NotFound"
}
/**
* An invalid input is included in a cursor field.
*/
case object MalformedCursor extends ClientFailure {
override val failureName: String = "MalformedCursor"
}
/**
* The operation did not succeed due to a closed gate
*/
case object ClosedGate extends ClientFailure {
override val failureName: String = "ClosedGate"
}
/**
* Server Failures are failures for which the owner of the product is responsible. Typically this
* means the request was valid but an issue within Product Mixer or a dependent service prevented
* it from succeeding.
*
* Server Failures contribute to the success rate SLO for the product.
*/
trait ServerFailure extends PipelineFailureCategory {
override val categoryName: String = "ServerFailure"
}
/**
* Unclassified failures occur when product code throws an exception that Product Mixer does not
* know how to classify.
*
* They can be used in failOpen policies, etc - but it's always preferred to instead add additional
* classification logic and to keep Unclassified failures at 0.
*/
case object UncategorizedServerFailure extends ServerFailure {
override val failureName: String = "UncategorizedServerFailure"
}
/**
* A hydrator or transformer returned a misconfigured feature map, this indicates a customer
* configuration error. The owner of the component should make sure the hydrator always returns a
* [[FeatureMap]] with the all features defined in the hydrator also set in the map, it should not have
* any unregistered features nor should registered features be absent.
*/
case object MisconfiguredFeatureMapFailure extends ServerFailure {
override val failureName: String = "MisconfiguredFeatureMapFailure"
}
/**
* A PipelineSelector returned an invalid ComponentIdentifier.
*
* A pipeline selector should choose the identifier of a pipeline that is contained by the 'pipelines'
* sequence of the ProductPipelineConfig.
*/
case object InvalidPipelineSelected extends ServerFailure {
override val failureName: String = "InvalidPipelineSelected"
}
/**
* Failures that occur when product code reaches an unexpected or otherwise illegal state.
*/
case object IllegalStateFailure extends ServerFailure {
override val failureName: String = "IllegalStateFailure"
}
/**
* An unexpected candidate was returned in a candidate source that was unable to be transformed.
*/
case object UnexpectedCandidateResult extends ServerFailure {
override val failureName: String = "UnexpectedCandidateResult"
}
/**
* An unexpected Candidate was returned in a marshaller
*/
case object UnexpectedCandidateInMarshaller extends ServerFailure {
override val failureName: String = "UnexpectedCandidateInMarshaller"
}
/**
* Pipeline execution failed due to an incorrectly configured quality factor (e.g, accessing
* quality factor state for a pipeline that does not have quality factor configured)
*/
case object MisconfiguredQualityFactor extends ServerFailure {
override val failureName: String = "MisconfiguredQualityFactor"
}
/**
* Pipeline execution failed due to an incorrectly configured decorator (e.g, decorator
* returned the wrong type or tried to decorate an already decorated candidate)
*/
case object MisconfiguredDecorator extends ServerFailure {
override val failureName: String = "MisconfiguredDecorator"
}
/**
* Candidate Source Pipeline execution failed due to a timeout.
*/
case object CandidateSourceTimeout extends ServerFailure {
override val failureName: String = "CandidateSourceTimeout"
}
/**
* Platform Failures are issues in the core Product Mixer logic itself which prevent a pipeline from
* properly executing. These failures are the responsibility of the Product Mixer team.
*/
trait PlatformFailure extends PipelineFailureCategory {
override val categoryName: String = "PlatformFailure"
}
/**
* Pipeline execution failed due to an unexpected error in Product Mixer.
*
* ExecutionFailed indicates a bug with the core Product Mixer execution logic rather than with a
* specific product. For example, a bug in PipelineBuilder leading to us returning a
* ProductPipelineResult that neither succeeded nor failed.
*/
case object ExecutionFailed extends PlatformFailure {
override val failureName: String = "ExecutionFailed"
}
/**
* Pipeline execution failed due to a feature hydration failure.
*
* FeatureHydrationFailed indicates that the underlying hydration for a feature defined in a hydrator
* failed (e.g, typically from a RPC call failing).
*/
case object FeatureHydrationFailed extends PlatformFailure {
override val failureName: String = "FeatureHydrationFailed"
}

View File

@ -1,13 +0,0 @@
package com.twitter.product_mixer.core.pipeline.pipeline_failure
/** Represents a way to classify a given [[Throwable]] to a [[PipelineFailure]] */
case class PipelineFailureClassifier(
classifier: PartialFunction[Throwable, PipelineFailure])
extends PartialFunction[Throwable, PipelineFailure] {
override def isDefinedAt(throwable: Throwable): Boolean = classifier.isDefinedAt(throwable)
override def apply(throwable: Throwable): PipelineFailure = classifier.apply(throwable)
}
private[core] object PipelineFailureClassifier {
val Empty: PipelineFailureClassifier = PipelineFailureClassifier(PartialFunction.empty)
}

View File

@ -1,67 +0,0 @@
package com.twitter.product_mixer.core.pipeline.pipeline_failure
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
private[pipeline_failure] class PipelineFailureSerializer()
extends JsonSerializer[PipelineFailure] {
private sealed trait BaseSerializableException
private case class SerializableException(
`class`: String,
message: String,
stackTrace: Seq[String],
cause: Option[BaseSerializableException])
extends BaseSerializableException
private case class SerializablePipelineFailure(
category: String,
reason: String,
underlying: Option[BaseSerializableException],
componentStack: Option[ComponentIdentifierStack],
stackTrace: Seq[String])
extends BaseSerializableException
private def serializeStackTrace(stackTrace: Array[StackTraceElement]): Seq[String] =
stackTrace.map(stackTraceElement => "at " + stackTraceElement.toString)
private def mkSerializableException(
t: Throwable,
recursionDepth: Int = 0
): Option[BaseSerializableException] = {
t match {
case _ if recursionDepth > 4 =>
// in the unfortunate case of a super deep chain of exceptions, stop if we get too deep
None
case pipelineFailure: PipelineFailure =>
Some(
SerializablePipelineFailure(
category =
pipelineFailure.category.categoryName + "/" + pipelineFailure.category.failureName,
reason = pipelineFailure.reason,
underlying =
pipelineFailure.underlying.flatMap(mkSerializableException(_, recursionDepth + 1)),
componentStack = pipelineFailure.componentStack,
stackTrace = serializeStackTrace(pipelineFailure.getStackTrace)
))
case t =>
Some(
SerializableException(
`class` = t.getClass.getName,
message = t.getMessage,
stackTrace = serializeStackTrace(t.getStackTrace),
cause = Option(t.getCause).flatMap(mkSerializableException(_, recursionDepth + 1))
)
)
}
}
override def serialize(
pipelineFailure: PipelineFailure,
gen: JsonGenerator,
serializers: SerializerProvider
): Unit = serializers.defaultSerializeValue(mkSerializableException(pipelineFailure), gen)
}

View File

@ -1,51 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/javax/inject:javax.inject",
"configapi/configapi-core",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/access_policy",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/mixer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/recommendation",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_execution_logger",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_selector_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/util",
"stitch/stitch-core",
"stringcenter/client/src/main/scala/com/twitter/stringcenter/client",
"stringcenter/client/src/main/scala/com/twitter/stringcenter/client/stitch",
],
exports = [
"3rdparty/jvm/javax/inject:javax.inject",
"configapi/configapi-core",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/access_policy",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_execution_logger",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/pipeline_selector_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"stitch/stitch-core",
],
)

View File

@ -1,28 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.product_mixer.core.functional_component.common.access_policy.WithDebugAccessPolicies
import com.twitter.product_mixer.core.model.common.identifier.ProductPipelineIdentifier
import com.twitter.product_mixer.core.model.marshalling.request.Request
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.stitch.Arrow
/**
* A Product Pipeline
*
* This is an abstract class, as we only construct these via the [[ProductPipelineBuilder]].
*
* A [[ProductPipeline]] is capable of processing a [[Request]] and returning a response.
*
* @tparam RequestType the domain model for the query or request
* @tparam ResponseType the final marshalled result type
*/
abstract class ProductPipeline[RequestType <: Request, ResponseType] private[product]
extends Pipeline[ProductPipelineRequest[RequestType], ResponseType]
with WithDebugAccessPolicies {
override private[core] val config: ProductPipelineConfig[RequestType, _, ResponseType]
override val arrow: Arrow[
ProductPipelineRequest[RequestType],
ProductPipelineResult[ResponseType]
]
override val identifier: ProductPipelineIdentifier
}

View File

@ -1,385 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finagle.tracing.Trace
import com.twitter.finagle.transport.Transport
import com.twitter.product_mixer.core.functional_component.common.access_policy.AccessPolicy
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.gate.DenyLoggedOutUsersGate
import com.twitter.product_mixer.core.gate.ParamGate
import com.twitter.product_mixer.core.gate.ParamGate.EnabledGateSuffix
import com.twitter.product_mixer.core.gate.ParamGate.SupportedClientGateSuffix
import com.twitter.product_mixer.core.model.common.Component
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.ProductPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.marshalling.request.Request
import com.twitter.product_mixer.core.pipeline.InvalidStepStateException
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.product_mixer.core.pipeline.PipelineBuilder
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.mixer.MixerPipelineBuilderFactory
import com.twitter.product_mixer.core.pipeline.mixer.MixerPipelineConfig
import com.twitter.product_mixer.core.pipeline.mixer.MixerPipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailureClassifier
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ProductDisabled
import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineBuilderFactory
import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineConfig
import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineResult
import com.twitter.product_mixer.core.quality_factor.HasQualityFactorStatus
import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver
import com.twitter.product_mixer.core.quality_factor.QualityFactorStatus
import com.twitter.product_mixer.core.service.Executor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.StoppedGateException
import com.twitter.product_mixer.core.service.pipeline_execution_logger.PipelineExecutionLogger
import com.twitter.product_mixer.core.service.pipeline_executor.PipelineExecutor
import com.twitter.product_mixer.core.service.pipeline_executor.PipelineExecutorRequest
import com.twitter.product_mixer.core.service.pipeline_executor.PipelineExecutorResult
import com.twitter.product_mixer.core.service.pipeline_selector_executor.PipelineSelectorExecutor
import com.twitter.product_mixer.core.service.pipeline_selector_executor.PipelineSelectorExecutorResult
import com.twitter.product_mixer.core.service.quality_factor_executor.QualityFactorExecutorResult
import com.twitter.stitch.Arrow
import com.twitter.stringcenter.client.StringCenterRequestContext
import com.twitter.stringcenter.client.stitch.StringCenterRequestContextLetter
import com.twitter.timelines.configapi.Params
import com.twitter.util.logging.Logging
import org.slf4j.MDC
class ProductPipelineBuilder[TRequest <: Request, Query <: PipelineQuery, Response](
gateExecutor: GateExecutor,
pipelineSelectorExecutor: PipelineSelectorExecutor,
pipelineExecutor: PipelineExecutor,
mixerPipelineBuilderFactory: MixerPipelineBuilderFactory,
recommendationPipelineBuilderFactory: RecommendationPipelineBuilderFactory,
override val statsReceiver: StatsReceiver,
pipelineExecutionLogger: PipelineExecutionLogger)
extends PipelineBuilder[ProductPipelineRequest[TRequest]]
with Logging { builder =>
override type UnderlyingResultType = Response
override type PipelineResultType = ProductPipelineResult[Response]
/**
* Query Transformer Step is implemented inline instead of using an executor.
*
* It's a simple, synchronous step that executes the query transformer.
*
* Since the output of the transformer is used in multiple other steps (Gate, Pipeline Execution),
* we've promoted the transformer to a step so that it's outputs can be reused easily.
*/
def pipelineQueryTransformerStep(
queryTransformer: (TRequest, Params) => Query,
context: Executor.Context
): Step[ProductPipelineRequest[TRequest], Query] =
new Step[ProductPipelineRequest[TRequest], Query] {
override def identifier: PipelineStepIdentifier =
ProductPipelineConfig.pipelineQueryTransformerStep
override def executorArrow: Arrow[ProductPipelineRequest[TRequest], Query] = {
wrapWithErrorHandling(context, identifier)(
Arrow.map[ProductPipelineRequest[TRequest], Query] {
case ProductPipelineRequest(request, params) => queryTransformer(request, params)
}
)
}
override def inputAdaptor(
query: ProductPipelineRequest[TRequest],
previousResult: ProductPipelineResult[Response]
): ProductPipelineRequest[TRequest] = query
override def resultUpdater(
previousPipelineResult: ProductPipelineResult[Response],
executorResult: Query
): ProductPipelineResult[Response] =
previousPipelineResult.copy(transformedQuery = Some(executorResult))
}
def qualityFactorStep(
qualityFactorStatus: QualityFactorStatus
): Step[Query, QualityFactorExecutorResult] = {
new Step[Query, QualityFactorExecutorResult] {
override def identifier: PipelineStepIdentifier = ProductPipelineConfig.qualityFactorStep
override def executorArrow: Arrow[Query, QualityFactorExecutorResult] =
Arrow
.map[Query, QualityFactorExecutorResult] { _ =>
QualityFactorExecutorResult(
pipelineQualityFactors =
qualityFactorStatus.qualityFactorByPipeline.mapValues(_.currentValue)
)
}
override def inputAdaptor(
query: ProductPipelineRequest[TRequest],
previousResult: ProductPipelineResult[Response]
): Query = previousResult.transformedQuery
.getOrElse {
throw InvalidStepStateException(identifier, "TransformedQuery")
}.asInstanceOf[Query]
override def resultUpdater(
previousPipelineResult: ProductPipelineResult[Response],
executorResult: QualityFactorExecutorResult
): ProductPipelineResult[Response] = {
previousPipelineResult.copy(
transformedQuery = previousPipelineResult.transformedQuery.map {
case queryWithQualityFactor: HasQualityFactorStatus =>
queryWithQualityFactor
.withQualityFactorStatus(qualityFactorStatus).asInstanceOf[Query]
case query =>
query
},
qualityFactorResult = Some(executorResult)
)
}
}
}
def gatesStep(
gates: Seq[Gate[Query]],
context: Executor.Context
): Step[Query, GateExecutorResult] = new Step[Query, GateExecutorResult] {
override def identifier: PipelineStepIdentifier = ProductPipelineConfig.gatesStep
override def executorArrow: Arrow[Query, GateExecutorResult] = {
gateExecutor.arrow(gates, context)
}
override def inputAdaptor(
query: ProductPipelineRequest[TRequest],
previousResult: ProductPipelineResult[Response]
): Query = previousResult.transformedQuery
.getOrElse {
throw InvalidStepStateException(identifier, "TransformedQuery")
}.asInstanceOf[Query]
override def resultUpdater(
previousPipelineResult: ProductPipelineResult[Response],
executorResult: GateExecutorResult
): ProductPipelineResult[Response] =
previousPipelineResult.copy(gateResult = Some(executorResult))
}
def pipelineSelectorStep(
pipelineByIdentifer: Map[ComponentIdentifier, Pipeline[Query, Response]],
pipelineSelector: Query => ComponentIdentifier,
context: Executor.Context
): Step[Query, PipelineSelectorExecutorResult] =
new Step[Query, PipelineSelectorExecutorResult] {
override def identifier: PipelineStepIdentifier = ProductPipelineConfig.pipelineSelectorStep
override def executorArrow: Arrow[
Query,
PipelineSelectorExecutorResult
] = pipelineSelectorExecutor.arrow(pipelineByIdentifer, pipelineSelector, context)
override def inputAdaptor(
query: ProductPipelineRequest[TRequest],
previousResult: ProductPipelineResult[Response]
): Query =
previousResult.transformedQuery
.getOrElse(throw InvalidStepStateException(identifier, "TransformedQuery")).asInstanceOf[
Query]
override def resultUpdater(
previousPipelineResult: ProductPipelineResult[Response],
executorResult: PipelineSelectorExecutorResult
): ProductPipelineResult[Response] =
previousPipelineResult.copy(pipelineSelectorResult = Some(executorResult))
}
def pipelineExecutionStep(
pipelineByIdentifier: Map[ComponentIdentifier, Pipeline[Query, Response]],
qualityFactorObserverByPipeline: Map[ComponentIdentifier, QualityFactorObserver],
context: Executor.Context
): Step[PipelineExecutorRequest[Query], PipelineExecutorResult[Response]] =
new Step[PipelineExecutorRequest[Query], PipelineExecutorResult[Response]] {
override def identifier: PipelineStepIdentifier = ProductPipelineConfig.pipelineExecutionStep
override def executorArrow: Arrow[
PipelineExecutorRequest[Query],
PipelineExecutorResult[Response]
] = {
pipelineExecutor.arrow(pipelineByIdentifier, qualityFactorObserverByPipeline, context)
}
override def inputAdaptor(
request: ProductPipelineRequest[TRequest],
previousResult: ProductPipelineResult[Response]
): PipelineExecutorRequest[Query] = {
val query = previousResult.transformedQuery
.getOrElse {
throw InvalidStepStateException(identifier, "TransformedQuery")
}.asInstanceOf[Query]
val pipelineIdentifier = previousResult.pipelineSelectorResult
.map(_.pipelineIdentifier).getOrElse {
throw InvalidStepStateException(identifier, "PipelineSelectorResult")
}
PipelineExecutorRequest(query, pipelineIdentifier)
}
override def resultUpdater(
previousPipelineResult: ProductPipelineResult[Response],
executorResult: PipelineExecutorResult[Response]
): ProductPipelineResult[Response] = {
val mixerPipelineResult = executorResult.pipelineResult match {
case mixerPipelineResult: MixerPipelineResult[Response] @unchecked =>
Some(mixerPipelineResult)
case _ =>
None
}
val recommendationPipelineResult = executorResult.pipelineResult match {
case recommendationPipelineResult: RecommendationPipelineResult[
_,
Response
] @unchecked =>
Some(recommendationPipelineResult)
case _ =>
None
}
previousPipelineResult.copy(
mixerPipelineResult = mixerPipelineResult,
recommendationPipelineResult = recommendationPipelineResult,
traceId = Trace.idOption.map(_.traceId.toString()),
result = executorResult.pipelineResult.result
)
}
}
def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
config: ProductPipelineConfig[TRequest, Query, Response]
): ProductPipeline[TRequest, Response] = {
val pipelineIdentifier = config.identifier
val context = Executor.Context(
PipelineFailureClassifier(
config.failureClassifier.orElse(StoppedGateException.classifier(ProductDisabled))),
parentComponentIdentifierStack.push(pipelineIdentifier)
)
val denyLoggedOutUsersGate = if (config.denyLoggedOutUsers) {
Some(DenyLoggedOutUsersGate(pipelineIdentifier))
} else {
None
}
val enabledGate: ParamGate =
ParamGate(pipelineIdentifier + EnabledGateSuffix, config.paramConfig.EnabledDeciderParam)
val supportedClientGate =
ParamGate(
pipelineIdentifier + SupportedClientGateSuffix,
config.paramConfig.SupportedClientParam)
/**
* Evaluate enabled decider gate first since if it's off, there is no reason to proceed
* Next evaluate supported client feature switch gate, followed by customer configured gates
*/
val allGates =
denyLoggedOutUsersGate.toSeq ++: enabledGate +: supportedClientGate +: config.gates
val childPipelines: Seq[Pipeline[Query, Response]] =
config.pipelines.map {
case mixerConfig: MixerPipelineConfig[Query, _, Response] =>
mixerConfig.build(context.componentStack, mixerPipelineBuilderFactory)
case recommendationConfig: RecommendationPipelineConfig[Query, _, _, Response] =>
recommendationConfig.build(context.componentStack, recommendationPipelineBuilderFactory)
case other =>
throw new IllegalArgumentException(
s"Product Pipelines only support Mixer and Recommendation pipelines, not $other")
}
val pipelineByIdentifier: Map[ComponentIdentifier, Pipeline[Query, Response]] =
childPipelines.map { pipeline =>
(pipeline.identifier, pipeline)
}.toMap
val qualityFactorStatus: QualityFactorStatus =
QualityFactorStatus.build(config.qualityFactorConfigs)
val qualityFactorObserverByPipeline = qualityFactorStatus.qualityFactorByPipeline.mapValues {
qualityFactor =>
qualityFactor.buildObserver()
}
buildGaugesForQualityFactor(pipelineIdentifier, qualityFactorStatus, statsReceiver)
/**
* Initialize MDC with access logging with everything we have at request time. We can put
* more stuff into MDC later down the pipeline, but at risk of exceptions/errors preventing
* them from being added
*/
val mdcInitArrow =
Arrow.map[ProductPipelineRequest[TRequest], ProductPipelineRequest[TRequest]] { request =>
val serviceIdentifier = ServiceIdentifier.fromCertificate(Transport.peerCertificate)
MDC.put("product", config.product.identifier.name)
MDC.put("serviceIdentifier", ServiceIdentifier.asString(serviceIdentifier))
request
}
val builtSteps = Seq(
pipelineQueryTransformerStep(config.pipelineQueryTransformer, context),
qualityFactorStep(qualityFactorStatus),
gatesStep(allGates, context),
pipelineSelectorStep(pipelineByIdentifier, config.pipelineSelector, context),
pipelineExecutionStep(pipelineByIdentifier, qualityFactorObserverByPipeline, context)
)
val underlying: Arrow[ProductPipelineRequest[TRequest], ProductPipelineResult[Response]] =
buildCombinedArrowFromSteps(
steps = builtSteps,
context = context,
initialEmptyResult = ProductPipelineResult.empty,
stepsInOrderFromConfig = ProductPipelineConfig.stepsInOrder
)
/**
* Unlike other components and pipelines, [[ProductPipeline]] must be observed in the
* [[ProductPipelineBuilder]] directly because the resulting [[ProductPipeline.arrow]]
* is run directly without an executor so must contain all stats.
*/
val observed =
wrapProductPipelineWithExecutorBookkeeping[
ProductPipelineRequest[TRequest],
ProductPipelineResult[Response]
](context, pipelineIdentifier)(underlying)
val finalArrow: Arrow[ProductPipelineRequest[TRequest], ProductPipelineResult[Response]] =
Arrow
.letWithArg[
ProductPipelineRequest[TRequest],
ProductPipelineResult[Response],
StringCenterRequestContext](StringCenterRequestContextLetter)(request =>
StringCenterRequestContext(
request.request.clientContext.languageCode,
request.request.clientContext.countryCode
))(
mdcInitArrow
.andThen(observed)
.onSuccess(result => result.transformedQuery.map(pipelineExecutionLogger(_, result))))
val configFromBuilder = config
new ProductPipeline[TRequest, Response] {
override private[core] val config: ProductPipelineConfig[TRequest, _, Response] =
configFromBuilder
override val arrow: Arrow[ProductPipelineRequest[TRequest], ProductPipelineResult[Response]] =
finalArrow
override val identifier: ProductPipelineIdentifier = pipelineIdentifier
override val alerts: Seq[Alert] = config.alerts
override val debugAccessPolicies: Set[AccessPolicy] = config.debugAccessPolicies
override val children: Seq[Component] = allGates ++ childPipelines
}
}
}

View File

@ -1,39 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.model.marshalling.request.Request
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.mixer.MixerPipelineBuilderFactory
import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineBuilderFactory
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.pipeline_execution_logger.PipelineExecutionLogger
import com.twitter.product_mixer.core.service.pipeline_executor.PipelineExecutor
import com.twitter.product_mixer.core.service.pipeline_selector_executor.PipelineSelectorExecutor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ProductPipelineBuilderFactory @Inject() (
gateExecutor: GateExecutor,
pipelineSelectorExecutor: PipelineSelectorExecutor,
pipelineExecutor: PipelineExecutor,
mixerPipelineBuilderFactory: MixerPipelineBuilderFactory,
recommendationPipelineBuilderFactory: RecommendationPipelineBuilderFactory,
statsReceiver: StatsReceiver,
pipelineExecutionLogger: PipelineExecutionLogger) {
def get[
TRequest <: Request,
Query <: PipelineQuery,
Response
]: ProductPipelineBuilder[TRequest, Query, Response] = {
new ProductPipelineBuilder[TRequest, Query, Response](
gateExecutor,
pipelineSelectorExecutor,
pipelineExecutor,
mixerPipelineBuilderFactory,
recommendationPipelineBuilderFactory,
statsReceiver,
pipelineExecutionLogger
)
}
}

View File

@ -1,107 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.product_mixer.core.functional_component.common.access_policy.AccessPolicy
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ProductPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.marshalling.request.Product
import com.twitter.product_mixer.core.model.marshalling.request.Request
import com.twitter.product_mixer.core.pipeline.PipelineConfig
import com.twitter.product_mixer.core.pipeline.PipelineConfigCompanion
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.product.ProductParamConfig
import com.twitter.product_mixer.core.quality_factor.QualityFactorConfig
import com.twitter.timelines.configapi.Params
trait ProductPipelineConfig[TRequest <: Request, Query <: PipelineQuery, Response]
extends PipelineConfig {
override val identifier: ProductPipelineIdentifier
val product: Product
val paramConfig: ProductParamConfig
/**
* Product Pipeline Gates will be executed before any other step (including retrieval from mixer
* pipelines). They're executed sequentially, and any "Stop" result will prevent pipeline execution.
*/
def gates: Seq[Gate[Query]] = Seq.empty
def pipelineQueryTransformer(request: TRequest, params: Params): Query
/**
* A list of all pipelines that power this product directly (there is no need to include pipelines
* called by those pipelines).
*
* Only pipeline from this list should referenced from the pipelineSelector
*/
def pipelines: Seq[PipelineConfig]
/**
* A pipeline selector selects a pipeline (from the list in `def pipelines`) to handle the
* current request.
*/
def pipelineSelector(query: Query): ComponentIdentifier
/**
** [[qualityFactorConfigs]] associates [[QualityFactorConfig]]s to specific pipelines
* using [[ComponentIdentifier]].
*/
def qualityFactorConfigs: Map[ComponentIdentifier, QualityFactorConfig] =
Map.empty
/**
* By default (for safety), product mixer pipelines do not allow logged out requests.
* A "DenyLoggedOutUsersGate" will be generated and added to the pipeline.
*
* You can disable this behavior by overriding `denyLoggedOutUsers` with False.
*/
val denyLoggedOutUsers: Boolean = true
/**
* A pipeline can define a partial function to rescue failures here. They will be treated as failures
* from a monitoring standpoint, and cancellation exceptions will always be propagated (they cannot be caught here).
*/
def failureClassifier: PartialFunction[Throwable, PipelineFailure] = PartialFunction.empty
/**
* Alerts can be used to indicate the pipeline's service level objectives. Alerts and
* dashboards will be automatically created based on this information.
*/
val alerts: Seq[Alert] = Seq.empty
/**
* Access Policies can be used to gate who can query a product from Product Mixer's query tool
* (go/turntable).
*
* This will typically be gated by an LDAP group associated with your team. For example:
*
* {{{
* override val debugAccessPolicies: Set[AccessPolicy] = Set(AllowedLdapGroups("NAME"))
* }}}
*
* You can disable all queries by using the [[com.twitter.product_mixer.core.functional_component.common.access_policy.BlockEverything]] policy.
*/
val debugAccessPolicies: Set[AccessPolicy]
}
object ProductPipelineConfig extends PipelineConfigCompanion {
val pipelineQueryTransformerStep: PipelineStepIdentifier = PipelineStepIdentifier(
"PipelineQueryTransformer")
val qualityFactorStep: PipelineStepIdentifier = PipelineStepIdentifier("QualityFactor")
val gatesStep: PipelineStepIdentifier = PipelineStepIdentifier("Gates")
val pipelineSelectorStep: PipelineStepIdentifier = PipelineStepIdentifier("PipelineSelector")
val pipelineExecutionStep: PipelineStepIdentifier = PipelineStepIdentifier("PipelineExecution")
/** All the Steps which are executed by a [[ProductPipeline]] in the order in which they are run */
override val stepsInOrder: Seq[PipelineStepIdentifier] = Seq(
pipelineQueryTransformerStep,
qualityFactorStep,
gatesStep,
pipelineSelectorStep,
pipelineExecutionStep
)
}

View File

@ -1,5 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.timelines.configapi.Params
case class ProductPipelineRequest[RequestType](request: RequestType, params: Params)

View File

@ -1,62 +0,0 @@
package com.twitter.product_mixer.core.pipeline.product
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.PipelineResult
import com.twitter.product_mixer.core.pipeline.mixer.MixerPipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.pipeline_selector_executor.PipelineSelectorExecutorResult
import com.twitter.product_mixer.core.service.quality_factor_executor.QualityFactorExecutorResult
case class ProductPipelineResult[Result](
transformedQuery: Option[PipelineQuery],
qualityFactorResult: Option[QualityFactorExecutorResult],
gateResult: Option[GateExecutorResult],
pipelineSelectorResult: Option[PipelineSelectorExecutorResult],
mixerPipelineResult: Option[MixerPipelineResult[Result]],
recommendationPipelineResult: Option[RecommendationPipelineResult[_, Result]],
traceId: Option[String],
failure: Option[PipelineFailure],
result: Option[Result])
extends PipelineResult[Result] {
override val resultSize: Int = {
if (mixerPipelineResult.isDefined) {
mixerPipelineResult.map(_.resultSize).getOrElse(0)
} else {
recommendationPipelineResult.map(_.resultSize).getOrElse(0)
}
}
override def withFailure(failure: PipelineFailure): PipelineResult[Result] =
copy(failure = Some(failure))
override def withResult(result: Result): PipelineResult[Result] = copy(result = Some(result))
}
object ProductPipelineResult {
def empty[A]: ProductPipelineResult[A] = ProductPipelineResult(
None,
None,
None,
None,
None,
None,
None,
None,
None
)
def fromResult[A](result: A): ProductPipelineResult[A] = ProductPipelineResult(
None,
None,
None,
None,
None,
None,
None,
None,
Some(result)
)
}

View File

@ -1,62 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/selector:insert_append_results",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featurestorev1",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/filter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/premarshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/side_effect",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/mixer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/scoring",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/async_feature_map_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/filter_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/scoring_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/selector_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/util",
"stitch/stitch-core",
],
exports = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/common/alert",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/premarshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/side_effect",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/quality_factor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/async_feature_map_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/candidate_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/filter_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/gate_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/quality_factor_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/query_feature_hydrator_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/scoring_pipeline_executor",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/selector_executor",
"stitch/stitch-core",
],
)

View File

@ -1,29 +0,0 @@
package com.twitter.product_mixer.core.pipeline.recommendation
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.RecommendationPipelineIdentifier
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.stitch.Arrow
/**
* A Recommendation Pipeline
*
* This is an abstract class, as we only construct these via the [[RecommendationPipelineBuilder]].
*
* A [[RecommendationPipeline]] is capable of processing requests (queries) and returning responses (results)
* in the correct format to directly send to users.
*
* @tparam Query the domain model for the query or request
* @tparam Candidate the type of the candidates
* @tparam Result the final marshalled result type
*/
abstract class RecommendationPipeline[
Query <: PipelineQuery,
Candidate <: UniversalNoun[Any],
Result]
extends Pipeline[Query, Result] {
override private[core] val config: RecommendationPipelineConfig[Query, Candidate, _, Result]
override val arrow: Arrow[Query, RecommendationPipelineResult[Candidate, Result]]
override val identifier: RecommendationPipelineIdentifier
}

View File

@ -1,67 +0,0 @@
package com.twitter.product_mixer.core.pipeline.recommendation
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineBuilderFactory
import com.twitter.product_mixer.core.pipeline.scoring.ScoringPipelineBuilderFactory
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutor
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutor
import com.twitter.product_mixer.core.service.domain_marshaller_executor.DomainMarshallerExecutor
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.pipeline_result_side_effect_executor.PipelineResultSideEffectExecutor
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutor
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.scoring_pipeline_executor.ScoringPipelineExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutor
import com.twitter.product_mixer.core.service.transport_marshaller_executor.TransportMarshallerExecutor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecommendationPipelineBuilderFactory @Inject() (
candidatePipelineExecutor: CandidatePipelineExecutor,
gateExecutor: GateExecutor,
selectorExecutor: SelectorExecutor,
queryFeatureHydratorExecutor: QueryFeatureHydratorExecutor,
asyncFeatureMapExecutor: AsyncFeatureMapExecutor,
candidateFeatureHydratorExecutor: CandidateFeatureHydratorExecutor,
filterExecutor: FilterExecutor,
scoringPipelineExecutor: ScoringPipelineExecutor,
candidateDecoratorExecutor: CandidateDecoratorExecutor,
domainMarshallerExecutor: DomainMarshallerExecutor,
transportMarshallerExecutor: TransportMarshallerExecutor,
pipelineResultSideEffectExecutor: PipelineResultSideEffectExecutor,
candidatePipelineBuilderFactory: CandidatePipelineBuilderFactory,
scoringPipelineBuilderFactory: ScoringPipelineBuilderFactory,
statsReceiver: StatsReceiver) {
def get[
Query <: PipelineQuery,
Candidate <: UniversalNoun[Any],
DomainResultType <: HasMarshalling,
Result
]: RecommendationPipelineBuilder[Query, Candidate, DomainResultType, Result] = {
new RecommendationPipelineBuilder[Query, Candidate, DomainResultType, Result](
candidatePipelineExecutor,
gateExecutor,
selectorExecutor,
queryFeatureHydratorExecutor,
asyncFeatureMapExecutor,
candidateFeatureHydratorExecutor,
filterExecutor,
scoringPipelineExecutor,
candidateDecoratorExecutor,
domainMarshallerExecutor,
transportMarshallerExecutor,
pipelineResultSideEffectExecutor,
candidatePipelineBuilderFactory,
scoringPipelineBuilderFactory,
statsReceiver
)
}
}

View File

@ -1,262 +0,0 @@
package com.twitter.product_mixer.core.pipeline.recommendation
import com.twitter.product_mixer.component_library.selector.InsertAppendResults
import com.twitter.product_mixer.core.functional_component.common.AllPipelines
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseCandidateFeatureHydrator
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseQueryFeatureHydrator
import com.twitter.product_mixer.core.functional_component.filter.Filter
import com.twitter.product_mixer.core.functional_component.gate.Gate
import com.twitter.product_mixer.core.functional_component.premarshaller.DomainMarshaller
import com.twitter.product_mixer.core.functional_component.selector.Selector
import com.twitter.product_mixer.core.functional_component.side_effect.PipelineResultSideEffect
import com.twitter.product_mixer.core.functional_component.marshaller.TransportMarshaller
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.RecommendationPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ScoringPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.FailOpenPolicy
import com.twitter.product_mixer.core.pipeline.PipelineConfig
import com.twitter.product_mixer.core.pipeline.PipelineConfigCompanion
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.candidate.CandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ClosedGate
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.pipeline.scoring.ScoringPipelineConfig
import com.twitter.product_mixer.core.quality_factor.QualityFactorConfig
/**
* This is the configuration necessary to generate a Recommendation Pipeline. Product code should create a
* RecommendationPipelineConfig, and then use a RecommendationPipelineBuilder to get the final RecommendationPipeline which can
* process requests.
*
* @tparam Query - The domain model for the query or request
* @tparam Candidate - The type of the candidates that the Candidate Pipelines are generating
* @tparam UnmarshalledResultType - The result type of the pipeline, but before marshalling to a wire protocol like URT
* @tparam Result - The final result that will be served to users
*/
trait RecommendationPipelineConfig[
Query <: PipelineQuery,
Candidate <: UniversalNoun[Any],
UnmarshalledResultType <: HasMarshalling,
Result]
extends PipelineConfig {
override val identifier: RecommendationPipelineIdentifier
/**
* Recommendation Pipeline Gates will be executed before any other step (including retrieval from candidate
* pipelines). They're executed sequentially, and any "Stop" result will prevent pipeline execution.
*/
def gates: Seq[Gate[Query]] = Seq.empty
/**
* A recommendation pipeline can fetch query-level features before candidate pipelines are executed.
*/
def fetchQueryFeatures: Seq[BaseQueryFeatureHydrator[Query, _]] = Seq.empty
/**
* Candidate pipelines retrieve candidates for possible inclusion in the result
*/
def fetchQueryFeaturesPhase2: Seq[BaseQueryFeatureHydrator[Query, _]] = Seq.empty
/**
* What candidate pipelines should this Recommendations Pipeline get candidate from?
*/
def candidatePipelines: Seq[CandidatePipelineConfig[Query, _, _, _]]
/**
* Dependent candidate pipelines to retrieve candidates that depend on the result of [[candidatePipelines]]
* [[DependentCandidatePipelineConfig]] have access to the list of previously retrieved & decorated
* candidates for use in constructing the query object.
*/
def dependentCandidatePipelines: Seq[DependentCandidatePipelineConfig[Query, _, _, _]] = Seq.empty
/**
* Takes final ranked list of candidates & apply any business logic (e.g, deduplicating and merging
* candidates before scoring).
*/
def postCandidatePipelinesSelectors: Seq[Selector[Query]] = Seq(InsertAppendResults(AllPipelines))
/**
* After selectors are run, you can fetch features for each candidate.
* The existing features from previous hydrations are passed in as inputs. You are not expected to
* put them into the resulting feature map yourself - they will be merged for you by the platform.
*/
def postCandidatePipelinesFeatureHydration: Seq[
BaseCandidateFeatureHydrator[Query, Candidate, _]
] =
Seq.empty
/**
* Global filters to run on all candidates.
*/
def globalFilters: Seq[Filter[Query, Candidate]] = Seq.empty
/**
* By default, a Recommendation Pipeline will fail closed - if any candidate or scoring
* pipeline fails to return a result, then the Recommendation Pipeline will not return a result.
* You can adjust this default policy, or provide specific policies to specific pipelines.
* Those specific policies will take priority.
*
* FailOpenPolicy.All will always fail open (the RecommendationPipeline will continue without that pipeline)
* FailOpenPolicy.Never will always fail closed (the RecommendationPipeline will fail if that pipeline fails)
*
* There's a default policy, and a specific Map of policies that takes precedence.
*/
def defaultFailOpenPolicy: FailOpenPolicy = FailOpenPolicy(Set(ClosedGate))
def candidatePipelineFailOpenPolicies: Map[CandidatePipelineIdentifier, FailOpenPolicy] =
Map.empty
def scoringPipelineFailOpenPolicies: Map[ScoringPipelineIdentifier, FailOpenPolicy] = Map.empty
/**
** [[qualityFactorConfigs]] associates [[QualityFactorConfig]]s to specific candidate pipelines
* using [[ComponentIdentifier]].
*/
def qualityFactorConfigs: Map[ComponentIdentifier, QualityFactorConfig] =
Map.empty
/**
* Scoring pipelines for scoring candidates.
* @note These do not drop or re-order candidates, you should do those in the sub-sequent selectors
* step based off of the scores on candidates set in those [[ScoringPipeline]]s.
*/
def scoringPipelines: Seq[ScoringPipelineConfig[Query, Candidate]]
/**
* Takes final ranked list of candidates & apply any business logic (e.g, capping number
* of ad accounts or pacing ad accounts).
*/
def resultSelectors: Seq[Selector[Query]]
/**
* Takes the final selected list of candidates and applies a final list of filters.
* Useful for doing very expensive filtering at the end of your pipeline.
*/
def postSelectionFilters: Seq[Filter[Query, Candidate]] = Seq.empty
/**
* Decorators allow for adding Presentations to candidates. While the Presentation can contain any
* arbitrary data, Decorators are often used to add a UrtItemPresentation for URT item support. Most
* customers will prefer to set a decorator in their respective candidate pipeline, however, a final
* global one is available for those that do global decoration as late possible to avoid unnecessary hydrations.
* @note This decorator can only return an ItemPresentation.
* @note This decorator cannot decorate an already decorated candidate from the prior decorator
* step in candidate pipelines.
*/
def decorator: Option[CandidateDecorator[Query, Candidate]] = None
/**
* Domain marshaller transforms the selections into the model expected by the marshaller
*/
def domainMarshaller: DomainMarshaller[Query, UnmarshalledResultType]
/**
* Mixer result side effects that are executed after selection and domain marshalling
*/
def resultSideEffects: Seq[PipelineResultSideEffect[Query, UnmarshalledResultType]] = Seq()
/**
* Transport marshaller transforms the model into our line-level API like URT or JSON
*/
def transportMarshaller: TransportMarshaller[UnmarshalledResultType, Result]
/**
* A pipeline can define a partial function to rescue failures here. They will be treated as failures
* from a monitoring standpoint, and cancellation exceptions will always be propagated (they cannot be caught here).
*/
def failureClassifier: PartialFunction[Throwable, PipelineFailure] = PartialFunction.empty
/**
* Alerts can be used to indicate the pipeline's service level objectives. Alerts and
* dashboards will be automatically created based on this information.
*/
val alerts: Seq[Alert] = Seq.empty
/**
* This method is used by the product mixer framework to build the pipeline.
*/
private[core] final def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
builder: RecommendationPipelineBuilderFactory
): RecommendationPipeline[Query, Candidate, Result] =
builder.get.build(parentComponentIdentifierStack, this)
}
object RecommendationPipelineConfig extends PipelineConfigCompanion {
val qualityFactorStep: PipelineStepIdentifier = PipelineStepIdentifier("QualityFactor")
val gatesStep: PipelineStepIdentifier = PipelineStepIdentifier("Gates")
val fetchQueryFeaturesStep: PipelineStepIdentifier = PipelineStepIdentifier("FetchQueryFeatures")
val fetchQueryFeaturesPhase2Step: PipelineStepIdentifier = PipelineStepIdentifier(
"FetchQueryFeaturesPhase2")
val candidatePipelinesStep: PipelineStepIdentifier = PipelineStepIdentifier("CandidatePipelines")
val dependentCandidatePipelinesStep: PipelineStepIdentifier =
PipelineStepIdentifier("DependentCandidatePipelines")
val postCandidatePipelinesSelectorsStep: PipelineStepIdentifier =
PipelineStepIdentifier("PostCandidatePipelinesSelectors")
val postCandidatePipelinesFeatureHydrationStep: PipelineStepIdentifier =
PipelineStepIdentifier("PostCandidatePipelinesFeatureHydration")
val globalFiltersStep: PipelineStepIdentifier = PipelineStepIdentifier("GlobalFilters")
val scoringPipelinesStep: PipelineStepIdentifier = PipelineStepIdentifier("ScoringPipelines")
val resultSelectorsStep: PipelineStepIdentifier = PipelineStepIdentifier("ResultSelectors")
val postSelectionFiltersStep: PipelineStepIdentifier = PipelineStepIdentifier(
"PostSelectionFilters")
val decoratorStep: PipelineStepIdentifier = PipelineStepIdentifier("Decorator")
val domainMarshallerStep: PipelineStepIdentifier = PipelineStepIdentifier("DomainMarshaller")
val resultSideEffectsStep: PipelineStepIdentifier = PipelineStepIdentifier("ResultSideEffects")
val transportMarshallerStep: PipelineStepIdentifier = PipelineStepIdentifier(
"TransportMarshaller")
/** All the Steps which are executed by a [[RecommendationPipeline]] in the order in which they are run */
override val stepsInOrder: Seq[PipelineStepIdentifier] = Seq(
qualityFactorStep,
gatesStep,
fetchQueryFeaturesStep,
fetchQueryFeaturesPhase2Step,
asyncFeaturesStep(candidatePipelinesStep),
candidatePipelinesStep,
asyncFeaturesStep(dependentCandidatePipelinesStep),
dependentCandidatePipelinesStep,
asyncFeaturesStep(postCandidatePipelinesSelectorsStep),
postCandidatePipelinesSelectorsStep,
asyncFeaturesStep(postCandidatePipelinesFeatureHydrationStep),
postCandidatePipelinesFeatureHydrationStep,
asyncFeaturesStep(globalFiltersStep),
globalFiltersStep,
asyncFeaturesStep(scoringPipelinesStep),
scoringPipelinesStep,
asyncFeaturesStep(resultSelectorsStep),
resultSelectorsStep,
asyncFeaturesStep(postSelectionFiltersStep),
postSelectionFiltersStep,
asyncFeaturesStep(decoratorStep),
decoratorStep,
domainMarshallerStep,
asyncFeaturesStep(resultSideEffectsStep),
resultSideEffectsStep,
transportMarshallerStep
)
/**
* All the Steps which an [[com.twitter.product_mixer.core.functional_component.feature_hydrator.AsyncHydrator AsyncHydrator]]
* can be configured to [[com.twitter.product_mixer.core.functional_component.feature_hydrator.AsyncHydrator.hydrateBefore hydrateBefore]]
*/
override val stepsAsyncFeatureHydrationCanBeCompletedBy: Set[PipelineStepIdentifier] = Set(
candidatePipelinesStep,
dependentCandidatePipelinesStep,
postCandidatePipelinesSelectorsStep,
postCandidatePipelinesFeatureHydrationStep,
globalFiltersStep,
scoringPipelinesStep,
resultSelectorsStep,
postSelectionFiltersStep,
decoratorStep,
resultSideEffectsStep,
)
}

View File

@ -1,84 +0,0 @@
package com.twitter.product_mixer.core.pipeline.recommendation
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.marshalling.HasMarshalling
import com.twitter.product_mixer.core.pipeline.PipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.service.async_feature_map_executor.AsyncFeatureMapExecutorResults
import com.twitter.product_mixer.core.service.candidate_decorator_executor.CandidateDecoratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.candidate_pipeline_executor.CandidatePipelineExecutorResult
import com.twitter.product_mixer.core.service.domain_marshaller_executor.DomainMarshallerExecutor
import com.twitter.product_mixer.core.service.filter_executor.FilterExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.pipeline_result_side_effect_executor.PipelineResultSideEffectExecutor
import com.twitter.product_mixer.core.service.quality_factor_executor.QualityFactorExecutorResult
import com.twitter.product_mixer.core.service.query_feature_hydrator_executor.QueryFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.scoring_pipeline_executor.ScoringPipelineExecutorResult
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
import com.twitter.product_mixer.core.service.transport_marshaller_executor.TransportMarshallerExecutor
case class RecommendationPipelineResult[Candidate <: UniversalNoun[Any], ResultType](
qualityFactorResult: Option[QualityFactorExecutorResult],
gateResult: Option[GateExecutorResult],
queryFeatures: Option[QueryFeatureHydratorExecutor.Result],
queryFeaturesPhase2: Option[QueryFeatureHydratorExecutor.Result],
mergedAsyncQueryFeatures: Option[AsyncFeatureMap],
candidatePipelineResults: Option[CandidatePipelineExecutorResult],
dependentCandidatePipelineResults: Option[CandidatePipelineExecutorResult],
postCandidatePipelinesSelectorResults: Option[SelectorExecutorResult],
postCandidatePipelinesFeatureHydrationResults: Option[
CandidateFeatureHydratorExecutorResult[Candidate]
],
globalFilterResults: Option[FilterExecutorResult[Candidate]],
scoringPipelineResults: Option[ScoringPipelineExecutorResult[Candidate]],
resultSelectorResults: Option[SelectorExecutorResult],
postSelectionFilterResults: Option[FilterExecutorResult[Candidate]],
candidateDecoratorResult: Option[CandidateDecoratorExecutorResult],
domainMarshallerResults: Option[DomainMarshallerExecutor.Result[HasMarshalling]],
resultSideEffectResults: Option[PipelineResultSideEffectExecutor.Result],
asyncFeatureHydrationResults: Option[AsyncFeatureMapExecutorResults],
transportMarshallerResults: Option[TransportMarshallerExecutor.Result[ResultType]],
failure: Option[PipelineFailure],
result: Option[ResultType])
extends PipelineResult[ResultType] {
override val resultSize: Int = result match {
case Some(seqResult @ Seq(_)) => seqResult.length
case Some(_) => 1
case None => 0
}
override def withFailure(
failure: PipelineFailure
): RecommendationPipelineResult[Candidate, ResultType] =
copy(failure = Some(failure))
override def withResult(result: ResultType): RecommendationPipelineResult[Candidate, ResultType] =
copy(result = Some(result))
}
object RecommendationPipelineResult {
def empty[A <: UniversalNoun[Any], B]: RecommendationPipelineResult[A, B] =
RecommendationPipelineResult(
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None
)
}

View File

@ -1,36 +0,0 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/selector:insert_append_results",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/state",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/candidate_feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/scorer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/util",
"stitch/stitch-core",
"util/util-core:scala",
],
exports = [
"3rdparty/jvm/javax/inject:javax.inject",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/candidate_feature_hydrator",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/gate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/scorer",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/step/selector",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice",
"stitch/stitch-core",
],
)

View File

@ -1,202 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.decorator.Decoration
import com.twitter.product_mixer.core.functional_component.scorer.ScoredCandidateResult
import com.twitter.product_mixer.core.gate.ParamGate
import com.twitter.product_mixer.core.gate.ParamGate._
import com.twitter.product_mixer.core.model.common.CandidateWithFeatures
import com.twitter.product_mixer.core.model.common.Component
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.common.identifier.ScoringPipelineIdentifier
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
import com.twitter.product_mixer.core.model.common.presentation.ItemCandidateWithDetails
import com.twitter.product_mixer.core.pipeline.NewPipelineBuilder
import com.twitter.product_mixer.core.pipeline.NewPipelineArrowBuilder
import com.twitter.product_mixer.core.pipeline.NewPipelineResult
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ClosedGate
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailureClassifier
import com.twitter.product_mixer.core.pipeline.state.HasCandidatesWithDetails
import com.twitter.product_mixer.core.pipeline.state.HasCandidatesWithFeatures
import com.twitter.product_mixer.core.pipeline.state.HasExecutorResults
import com.twitter.product_mixer.core.pipeline.state.HasQuery
import com.twitter.product_mixer.core.pipeline.state.HasResult
import com.twitter.product_mixer.core.pipeline.step.candidate_feature_hydrator.CandidateFeatureHydratorStep
import com.twitter.product_mixer.core.pipeline.step.gate.GateStep
import com.twitter.product_mixer.core.pipeline.step.scorer.ScorerStep
import com.twitter.product_mixer.core.pipeline.step.selector.SelectorStep
import com.twitter.product_mixer.core.service.Executor
import com.twitter.product_mixer.core.service.ExecutorResult
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.StoppedGateException
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
import com.twitter.stitch.Arrow
import javax.inject.Inject
import scala.collection.immutable.ListMap
/**
* NewScoringPipelineBuilder builds [[ScoringPipeline]]s from [[ScoringPipelineConfig]]s.
* New because it's meant to eventually replace [[ScoringPipelineBuilder]]
* You should inject a [[ScoringPipelineBuilderFactory]] and call `.get` to build these.
*
* @see [[ScoringPipelineConfig]] for the description of the type parameters
* @tparam Query the type of query these accept.
* @tparam Candidate the domain model for the candidate being scored
*/
class NewScoringPipelineBuilder[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]] @Inject() (
selectionStep: SelectorStep[Query, ScoringPipelineState[Query, Candidate]],
gateStep: GateStep[Query, ScoringPipelineState[Query, Candidate]],
candidateFeatureHydrationStep: CandidateFeatureHydratorStep[
Query,
Candidate,
ScoringPipelineState[Query, Candidate]
],
scorerStep: ScorerStep[Query, Candidate, ScoringPipelineState[Query, Candidate]])
extends NewPipelineBuilder[ScoringPipelineConfig[Query, Candidate], Seq[
CandidateWithFeatures[Candidate]
], ScoringPipelineState[Query, Candidate], ScoringPipeline[Query, Candidate]] {
override def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
arrowBuilder: NewPipelineArrowBuilder[ArrowResult, ArrowState],
scoringPipelineConfig: ScoringPipelineConfig[Query, Candidate]
): ScoringPipeline[Query, Candidate] = {
val pipelineIdentifier = scoringPipelineConfig.identifier
val context = Executor.Context(
PipelineFailureClassifier(
scoringPipelineConfig.failureClassifier.orElse(
StoppedGateException.classifier(ClosedGate))),
parentComponentIdentifierStack.push(pipelineIdentifier)
)
val enabledGateOpt = scoringPipelineConfig.enabledDeciderParam.map { deciderParam =>
ParamGate(pipelineIdentifier + EnabledGateSuffix, deciderParam)
}
val supportedClientGateOpt = scoringPipelineConfig.supportedClientParam.map { param =>
ParamGate(pipelineIdentifier + SupportedClientGateSuffix, param)
}
/**
* Evaluate enabled decider gate first since if it's off, there is no reason to proceed
* Next evaluate supported client feature switch gate, followed by customer configured gates
*/
val allGates =
enabledGateOpt.toSeq ++ supportedClientGateOpt.toSeq ++ scoringPipelineConfig.gates
val underlyingArrow = arrowBuilder
.add(ScoringPipelineConfig.gatesStep, gateStep, allGates)
.add(ScoringPipelineConfig.selectorsStep, selectionStep, scoringPipelineConfig.selectors)
.add(
ScoringPipelineConfig.preScoringFeatureHydrationPhase1Step,
candidateFeatureHydrationStep,
scoringPipelineConfig.preScoringFeatureHydrationPhase1)
.add(
ScoringPipelineConfig.preScoringFeatureHydrationPhase2Step,
candidateFeatureHydrationStep,
scoringPipelineConfig.preScoringFeatureHydrationPhase2)
.add(ScoringPipelineConfig.scorersStep, scorerStep, scoringPipelineConfig.scorers).buildArrow(
context)
val finalArrow = Arrow
.map { inputs: ScoringPipeline.Inputs[Query] =>
ScoringPipelineState[Query, Candidate](inputs.query, inputs.candidates, ListMap.empty)
}.andThen(underlyingArrow).map { pipelineResult =>
ScoringPipelineResult(
gateResults = pipelineResult.executorResultsByPipelineStep
.get(ScoringPipelineConfig.gatesStep)
.map(_.asInstanceOf[GateExecutorResult]),
selectorResults = pipelineResult.executorResultsByPipelineStep
.get(ScoringPipelineConfig.selectorsStep)
.map(_.asInstanceOf[SelectorExecutorResult]),
preScoringHydrationPhase1Result = pipelineResult.executorResultsByPipelineStep
.get(ScoringPipelineConfig.preScoringFeatureHydrationPhase1Step)
.map(_.asInstanceOf[CandidateFeatureHydratorExecutorResult[Candidate]]),
preScoringHydrationPhase2Result = pipelineResult.executorResultsByPipelineStep
.get(ScoringPipelineConfig.preScoringFeatureHydrationPhase2Step)
.map(_.asInstanceOf[CandidateFeatureHydratorExecutorResult[Candidate]]),
scorerResults = pipelineResult.executorResultsByPipelineStep
.get(ScoringPipelineConfig.scorersStep)
.map(_.asInstanceOf[CandidateFeatureHydratorExecutorResult[Candidate]]),
failure = pipelineResult match {
case failure: NewPipelineResult.Failure =>
Some(failure.failure)
case _ => None
},
result = pipelineResult match {
case result: NewPipelineResult.Success[Seq[CandidateWithFeatures[Candidate]]] =>
Some(result.result.map { candidateWithFeatures =>
ScoredCandidateResult(
candidateWithFeatures.candidate,
candidateWithFeatures.features)
})
case _ => None
}
)
}
new ScoringPipeline[Query, Candidate] {
override val arrow: Arrow[ScoringPipeline.Inputs[Query], ScoringPipelineResult[Candidate]] =
finalArrow
override val identifier: ScoringPipelineIdentifier = scoringPipelineConfig.identifier
override val alerts: Seq[Alert] = scoringPipelineConfig.alerts
override val children: Seq[Component] =
allGates ++ scoringPipelineConfig.preScoringFeatureHydrationPhase1 ++ scoringPipelineConfig.preScoringFeatureHydrationPhase2 ++ scoringPipelineConfig.scorers
override private[core] val config = scoringPipelineConfig
}
}
}
case class ScoringPipelineState[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]](
override val query: Query,
candidates: Seq[ItemCandidateWithDetails],
override val executorResultsByPipelineStep: ListMap[PipelineStepIdentifier, ExecutorResult])
extends HasQuery[Query, ScoringPipelineState[Query, Candidate]]
with HasCandidatesWithDetails[ScoringPipelineState[Query, Candidate]]
with HasCandidatesWithFeatures[Candidate, ScoringPipelineState[Query, Candidate]]
with HasExecutorResults[ScoringPipelineState[Query, Candidate]]
with HasResult[Seq[CandidateWithFeatures[Candidate]]] {
override val candidatesWithDetails: Seq[CandidateWithDetails] = candidates
override val candidatesWithFeatures: Seq[CandidateWithFeatures[Candidate]] =
candidates.asInstanceOf[Seq[CandidateWithFeatures[Candidate]]]
override val buildResult: Seq[CandidateWithFeatures[Candidate]] = candidatesWithFeatures
override def updateCandidatesWithDetails(
newCandidates: Seq[CandidateWithDetails]
): ScoringPipelineState[Query, Candidate] = {
this.copy(candidates = newCandidates.asInstanceOf[Seq[ItemCandidateWithDetails]])
}
override def updateQuery(newQuery: Query): ScoringPipelineState[Query, Candidate] =
this.copy(query = newQuery)
override def updateDecorations(
decoration: Seq[Decoration]
): ScoringPipelineState[Query, Candidate] = ???
override def updateCandidatesWithFeatures(
newCandidates: Seq[CandidateWithFeatures[Candidate]]
): ScoringPipelineState[Query, Candidate] = {
val updatedCandidates = candidates.zip(newCandidates).map {
case (itemCandidateWithDetails, newCandidate) =>
itemCandidateWithDetails.copy(features =
itemCandidateWithDetails.features ++ newCandidate.features)
}
this.copy(query, updatedCandidates)
}
override private[pipeline] def setExecutorResults(
newMap: ListMap[PipelineStepIdentifier, ExecutorResult]
) = this.copy(executorResultsByPipelineStep = newMap)
}

View File

@ -1,32 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.product_mixer.core.functional_component.scorer.ScoredCandidateResult
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.ScoringPipelineIdentifier
import com.twitter.product_mixer.core.model.common.presentation.ItemCandidateWithDetails
import com.twitter.product_mixer.core.pipeline.Pipeline
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.stitch.Arrow
/**
* A Scoring Pipeline
*
* This is an abstract class, as we only construct these via the [[ScoringPipelineBuilder]].
*
* A [[ScoringPipeline]] is capable of pre-filtering candidates for scoring, performing the scoring
* then running selection heuristics (ranking, dropping, etc) based off of the score.
* @tparam Query the domain model for the query or request
* @tparam Candidate the domain model for the candidate being scored
*/
abstract class ScoringPipeline[-Query <: PipelineQuery, Candidate <: UniversalNoun[Any]]
extends Pipeline[ScoringPipeline.Inputs[Query], Seq[ScoredCandidateResult[Candidate]]] {
override private[core] val config: ScoringPipelineConfig[Query, Candidate]
override val arrow: Arrow[ScoringPipeline.Inputs[Query], ScoringPipelineResult[Candidate]]
override val identifier: ScoringPipelineIdentifier
}
object ScoringPipeline {
case class Inputs[+Query <: PipelineQuery](
query: Query,
candidates: Seq[ItemCandidateWithDetails])
}

View File

@ -1,367 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.scorer.ScoredCandidateResult
import com.twitter.product_mixer.core.gate.ParamGate
import com.twitter.product_mixer.core.gate.ParamGate.EnabledGateSuffix
import com.twitter.product_mixer.core.gate.ParamGate.SupportedClientGateSuffix
import com.twitter.product_mixer.core.model.common.CandidateWithFeatures
import com.twitter.product_mixer.core.model.common.Component
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.ScoringPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.model.common.presentation.ItemCandidateWithDetails
import com.twitter.product_mixer.core.pipeline.InvalidStepStateException
import com.twitter.product_mixer.core.pipeline.PipelineBuilder
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.ClosedGate
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailureClassifier
import com.twitter.product_mixer.core.pipeline.scoring.ScoringPipeline.Inputs
import com.twitter.product_mixer.core.service.Executor
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.StoppedGateException
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
import com.twitter.stitch.Arrow
import javax.inject.Inject
/**
* ScoringPipelineBuilder builds [[ScoringPipeline]]s from [[ScoringPipelineConfig]]s.
*
* You should inject a [[ScoringPipelineBuilderFactory]] and call `.get` to build these.
*
* @see [[ScoringPipelineConfig]] for the description of the type parameters
* @tparam Query the type of query these accept.
* @tparam Candidate the domain model for the candidate being scored
*/
class ScoringPipelineBuilder[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]] @Inject() (
gateExecutor: GateExecutor,
selectorExecutor: SelectorExecutor,
candidateFeatureHydratorExecutor: CandidateFeatureHydratorExecutor,
override val statsReceiver: StatsReceiver)
extends PipelineBuilder[Inputs[Query]] {
override type UnderlyingResultType = Seq[ScoredCandidateResult[Candidate]]
override type PipelineResultType = ScoringPipelineResult[Candidate]
def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
config: ScoringPipelineConfig[Query, Candidate]
): ScoringPipeline[Query, Candidate] = {
val pipelineIdentifier = config.identifier
val context = Executor.Context(
PipelineFailureClassifier(
config.failureClassifier.orElse(StoppedGateException.classifier(ClosedGate))),
parentComponentIdentifierStack.push(pipelineIdentifier)
)
val enabledGateOpt = config.enabledDeciderParam.map { deciderParam =>
ParamGate(pipelineIdentifier + EnabledGateSuffix, deciderParam)
}
val supportedClientGateOpt = config.supportedClientParam.map { param =>
ParamGate(pipelineIdentifier + SupportedClientGateSuffix, param)
}
/**
* Evaluate enabled decider gate first since if it's off, there is no reason to proceed
* Next evaluate supported client feature switch gate, followed by customer configured gates
*/
val allGates = enabledGateOpt.toSeq ++ supportedClientGateOpt.toSeq ++ config.gates
val GatesStep = new Step[Query, GateExecutorResult] {
override def identifier: PipelineStepIdentifier = ScoringPipelineConfig.gatesStep
override lazy val executorArrow: Arrow[Query, GateExecutorResult] =
gateExecutor.arrow(allGates, context)
override def inputAdaptor(
query: ScoringPipeline.Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): Query = {
query.query
}
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: GateExecutorResult
): ScoringPipelineResult[Candidate] =
previousPipelineResult.copy(gateResults = Some(executorResult))
}
val SelectorsStep = new Step[SelectorExecutor.Inputs[Query], SelectorExecutorResult] {
override def identifier: PipelineStepIdentifier = ScoringPipelineConfig.selectorsStep
override def executorArrow: Arrow[SelectorExecutor.Inputs[Query], SelectorExecutorResult] =
selectorExecutor.arrow(config.selectors, context)
override def inputAdaptor(
query: ScoringPipeline.Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): SelectorExecutor.Inputs[Query] = SelectorExecutor.Inputs(query.query, query.candidates)
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: SelectorExecutorResult
): ScoringPipelineResult[Candidate] =
previousPipelineResult.copy(selectorResults = Some(executorResult))
}
val PreScoringFeatureHydrationPhase1Step =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[Candidate]
] {
override def identifier: PipelineStepIdentifier =
ScoringPipelineConfig.preScoringFeatureHydrationPhase1Step
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[Candidate]
] =
candidateFeatureHydratorExecutor.arrow(config.preScoringFeatureHydrationPhase1, context)
override def inputAdaptor(
query: ScoringPipeline.Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): CandidateFeatureHydratorExecutor.Inputs[Query, Candidate] = {
val selectedCandidatesResult = previousResult.selectorResults.getOrElse {
throw InvalidStepStateException(identifier, "SelectorResults")
}.selectedCandidates
CandidateFeatureHydratorExecutor.Inputs(
query.query,
selectedCandidatesResult.asInstanceOf[Seq[CandidateWithFeatures[Candidate]]])
}
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: CandidateFeatureHydratorExecutorResult[Candidate]
): ScoringPipelineResult[Candidate] = previousPipelineResult.copy(
preScoringHydrationPhase1Result = Some(executorResult)
)
}
val PreScoringFeatureHydrationPhase2Step =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[Candidate]
] {
override def identifier: PipelineStepIdentifier =
ScoringPipelineConfig.preScoringFeatureHydrationPhase2Step
override def executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[Candidate]
] =
candidateFeatureHydratorExecutor.arrow(config.preScoringFeatureHydrationPhase2, context)
override def inputAdaptor(
query: ScoringPipeline.Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): CandidateFeatureHydratorExecutor.Inputs[Query, Candidate] = {
val preScoringHydrationPhase1FeatureMaps: Seq[FeatureMap] =
previousResult.preScoringHydrationPhase1Result
.getOrElse(
throw InvalidStepStateException(identifier, "PreScoringHydrationPhase1Result"))
.results.map(_.features)
val itemCandidates = previousResult.selectorResults
.getOrElse(throw InvalidStepStateException(identifier, "SelectionResults"))
.selectedCandidates.collect {
case itemCandidate: ItemCandidateWithDetails => itemCandidate
}
// If there is no feature hydration (empty results), no need to attempt merging.
val candidates = if (preScoringHydrationPhase1FeatureMaps.isEmpty) {
itemCandidates
} else {
itemCandidates.zip(preScoringHydrationPhase1FeatureMaps).map {
case (itemCandidate, featureMap) =>
itemCandidate.copy(features = itemCandidate.features ++ featureMap)
}
}
CandidateFeatureHydratorExecutor.Inputs(
query.query,
candidates.asInstanceOf[Seq[CandidateWithFeatures[Candidate]]])
}
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: CandidateFeatureHydratorExecutorResult[Candidate]
): ScoringPipelineResult[Candidate] = previousPipelineResult.copy(
preScoringHydrationPhase2Result = Some(executorResult)
)
}
def getMergedPreScoringFeatureMap(
stepIdentifier: PipelineStepIdentifier,
previousResult: ScoringPipelineResult[Candidate]
): Seq[FeatureMap] = {
val preScoringHydrationPhase1FeatureMaps: Seq[FeatureMap] =
previousResult.preScoringHydrationPhase1Result
.getOrElse(
throw InvalidStepStateException(
stepIdentifier,
"PreScoringHydrationPhase1Result")).results.map(_.features)
val preScoringHydrationPhase2FeatureMaps: Seq[FeatureMap] =
previousResult.preScoringHydrationPhase2Result
.getOrElse(
throw InvalidStepStateException(
stepIdentifier,
"PreScoringHydrationPhase2Result")).results.map(_.features)
/*
* If either pre-scoring hydration phase feature map is empty, no need to merge them,
* we can just take all non-empty ones.
*/
if (preScoringHydrationPhase1FeatureMaps.isEmpty) {
preScoringHydrationPhase2FeatureMaps
} else if (preScoringHydrationPhase2FeatureMaps.isEmpty) {
preScoringHydrationPhase1FeatureMaps
} else {
// No need to check the size in both, since the inputs to both hydration phases are the
// same and each phase ensures the number of candidates and ordering matches the input.
preScoringHydrationPhase1FeatureMaps.zip(preScoringHydrationPhase2FeatureMaps).map {
case (preScoringHydrationPhase1FeatureMap, preScoringHydrationPhasesFeatureMap) =>
preScoringHydrationPhase1FeatureMap ++ preScoringHydrationPhasesFeatureMap
}
}
}
val ScorersStep =
new Step[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[Candidate]
] {
override def identifier: PipelineStepIdentifier = ScoringPipelineConfig.scorersStep
override def inputAdaptor(
query: ScoringPipeline.Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): CandidateFeatureHydratorExecutor.Inputs[Query, Candidate] = {
val mergedPreScoringFeatureHydrationFeatures: Seq[FeatureMap] =
getMergedPreScoringFeatureMap(ScoringPipelineConfig.scorersStep, previousResult)
val itemCandidates = previousResult.selectorResults
.getOrElse(throw InvalidStepStateException(identifier, "SelectionResults"))
.selectedCandidates.collect {
case itemCandidate: ItemCandidateWithDetails => itemCandidate
}
// If there was no pre-scoring features hydration, no need to re-merge feature maps
// and construct a new item candidate
val updatedCandidates = if (mergedPreScoringFeatureHydrationFeatures.isEmpty) {
itemCandidates
} else {
itemCandidates.zip(mergedPreScoringFeatureHydrationFeatures).map {
case (itemCandidate, preScoringFeatureMap) =>
itemCandidate.copy(features = itemCandidate.features ++ preScoringFeatureMap)
}
}
CandidateFeatureHydratorExecutor.Inputs(
query.query,
updatedCandidates.asInstanceOf[Seq[CandidateWithFeatures[Candidate]]])
}
override lazy val executorArrow: Arrow[
CandidateFeatureHydratorExecutor.Inputs[Query, Candidate],
CandidateFeatureHydratorExecutorResult[
Candidate
]
] = candidateFeatureHydratorExecutor.arrow(config.scorers.toSeq, context)
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: CandidateFeatureHydratorExecutorResult[Candidate]
): ScoringPipelineResult[Candidate] =
previousPipelineResult.copy(scorerResults = Some(executorResult))
}
val ResultStep =
new Step[Seq[CandidateWithFeatures[UniversalNoun[Any]]], Seq[
CandidateWithFeatures[UniversalNoun[Any]]
]] {
override def identifier: PipelineStepIdentifier = ScoringPipelineConfig.resultStep
override def executorArrow: Arrow[Seq[CandidateWithFeatures[UniversalNoun[Any]]], Seq[
CandidateWithFeatures[UniversalNoun[Any]]
]] = Arrow.identity
override def inputAdaptor(
query: Inputs[Query],
previousResult: ScoringPipelineResult[Candidate]
): Seq[CandidateWithFeatures[UniversalNoun[Any]]] = previousResult.selectorResults
.getOrElse(throw InvalidStepStateException(identifier, "SelectionResults"))
.selectedCandidates.collect {
case itemCandidate: ItemCandidateWithDetails => itemCandidate
}
override def resultUpdater(
previousPipelineResult: ScoringPipelineResult[Candidate],
executorResult: Seq[CandidateWithFeatures[UniversalNoun[Any]]]
): ScoringPipelineResult[Candidate] = {
val scorerResults: Seq[FeatureMap] = previousPipelineResult.scorerResults
.getOrElse(throw InvalidStepStateException(identifier, "ScorerResult")).results.map(
_.features)
val mergedPreScoringFeatureHydrationFeatureMaps: Seq[FeatureMap] =
getMergedPreScoringFeatureMap(ScoringPipelineConfig.resultStep, previousPipelineResult)
val itemCandidates = executorResult.asInstanceOf[Seq[ItemCandidateWithDetails]]
val finalFeatureMap = if (mergedPreScoringFeatureHydrationFeatureMaps.isEmpty) {
scorerResults
} else {
scorerResults
.zip(mergedPreScoringFeatureHydrationFeatureMaps).map {
case (preScoringFeatureMap, scoringFeatureMap) =>
preScoringFeatureMap ++ scoringFeatureMap
}
}
val finalResults = itemCandidates.zip(finalFeatureMap).map {
case (itemCandidate, featureMap) =>
ScoredCandidateResult(itemCandidate.candidate.asInstanceOf[Candidate], featureMap)
}
previousPipelineResult.withResult(finalResults)
}
}
val builtSteps = Seq(
GatesStep,
SelectorsStep,
PreScoringFeatureHydrationPhase1Step,
PreScoringFeatureHydrationPhase2Step,
ScorersStep,
ResultStep
)
/** The main execution logic for this Candidate Pipeline. */
val finalArrow: Arrow[ScoringPipeline.Inputs[Query], ScoringPipelineResult[Candidate]] =
buildCombinedArrowFromSteps(
steps = builtSteps,
context = context,
initialEmptyResult = ScoringPipelineResult.empty,
stepsInOrderFromConfig = ScoringPipelineConfig.stepsInOrder
)
val configFromBuilder = config
new ScoringPipeline[Query, Candidate] {
override private[core] val config: ScoringPipelineConfig[Query, Candidate] = configFromBuilder
override val arrow: Arrow[ScoringPipeline.Inputs[Query], ScoringPipelineResult[Candidate]] =
finalArrow
override val identifier: ScoringPipelineIdentifier = pipelineIdentifier
override val alerts: Seq[Alert] = config.alerts
override val children: Seq[Component] =
allGates ++ config.preScoringFeatureHydrationPhase1 ++ config.preScoringFeatureHydrationPhase2 ++ config.scorers
}
}
}

View File

@ -1,30 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutor
import com.twitter.product_mixer.core.service.gate_executor.GateExecutor
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ScoringPipelineBuilderFactory @Inject() (
gateExecutor: GateExecutor,
selectorExecutor: SelectorExecutor,
candidateFeatureHydratorExecutor: CandidateFeatureHydratorExecutor,
statsReceiver: StatsReceiver) {
def get[
Query <: PipelineQuery,
Candidate <: UniversalNoun[Any]
]: ScoringPipelineBuilder[Query, Candidate] = {
new ScoringPipelineBuilder[Query, Candidate](
gateExecutor,
selectorExecutor,
candidateFeatureHydratorExecutor,
statsReceiver
)
}
}

View File

@ -1,131 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.product_mixer.component_library.selector.InsertAppendResults
import com.twitter.product_mixer.core.functional_component.common.AllPipelines
import com.twitter.product_mixer.core.functional_component.common.alert.Alert
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseCandidateFeatureHydrator
import com.twitter.product_mixer.core.functional_component.gate.BaseGate
import com.twitter.product_mixer.core.functional_component.scorer.Scorer
import com.twitter.product_mixer.core.functional_component.selector.Selector
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.model.common.identifier.ComponentIdentifierStack
import com.twitter.product_mixer.core.model.common.identifier.ScoringPipelineIdentifier
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.pipeline.PipelineConfig
import com.twitter.product_mixer.core.pipeline.PipelineConfigCompanion
import com.twitter.product_mixer.core.pipeline.PipelineQuery
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.decider.DeciderParam
/**
* This is the configuration necessary to generate a Scoring Pipeline. Product code should create a
* ScoringPipelineConfig, and then use a ScoringPipelineBuilder to get the final ScoringPipeline which can
* process requests.
*
* @tparam Query - The domain model for the query or request
* @tparam Candidate the domain model for the candidate being scored
*/
trait ScoringPipelineConfig[-Query <: PipelineQuery, Candidate <: UniversalNoun[Any]]
extends PipelineConfig {
override val identifier: ScoringPipelineIdentifier
/**
* When these Params are defined, they will automatically be added as Gates in the pipeline
* by the CandidatePipelineBuilder
*
* The enabled decider param can to be used to quickly disable a Candidate Pipeline via Decider
*/
val enabledDeciderParam: Option[DeciderParam[Boolean]] = None
/**
* This supported client feature switch param can be used with a Feature Switch to control the
* rollout of a new Candidate Pipeline from dogfood to experiment to production
*/
val supportedClientParam: Option[FSParam[Boolean]] = None
/** [[BaseGate]]s that are applied sequentially, the pipeline will only run if all the Gates are open */
def gates: Seq[BaseGate[Query]] = Seq.empty
/**
* Logic for selecting which candidates to score. Note, this doesn't drop the candidates from
* the final result, just whether to score it in this pipeline or not.
*/
def selectors: Seq[Selector[Query]]
/**
* After selectors are run, you can fetch features for each candidate.
* The existing features from previous hydrations are passed in as inputs. You are not expected to
* put them into the resulting feature map yourself - they will be merged for you by the platform.
*/
def preScoringFeatureHydrationPhase1: Seq[BaseCandidateFeatureHydrator[Query, Candidate, _]] =
Seq.empty
/**
* A second phase of feature hydration that can be run after selection and after the first phase
* of pre-scoring feature hydration. You are not expected to put them into the resulting
* feature map yourself - they will be merged for you by the platform.
*/
def preScoringFeatureHydrationPhase2: Seq[BaseCandidateFeatureHydrator[Query, Candidate, _]] =
Seq.empty
/**
* Ranker Function for candidates. Scorers are executed in parallel.
* Note: Order does not matter, this could be a Set if Set was covariant over it's type.
*/
def scorers: Seq[Scorer[Query, Candidate]]
/**
* A pipeline can define a partial function to rescue failures here. They will be treated as failures
* from a monitoring standpoint, and cancellation exceptions will always be propagated (they cannot be caught here).
*/
def failureClassifier: PartialFunction[Throwable, PipelineFailure] = PartialFunction.empty
/**
* Alerts can be used to indicate the pipeline's service level objectives. Alerts and
* dashboards will be automatically created based on this information.
*/
val alerts: Seq[Alert] = Seq.empty
/**
* This method is used by the product mixer framework to build the pipeline.
*/
private[core] final def build(
parentComponentIdentifierStack: ComponentIdentifierStack,
builder: ScoringPipelineBuilderFactory
): ScoringPipeline[Query, Candidate] =
builder.get.build(parentComponentIdentifierStack, this)
}
object ScoringPipelineConfig extends PipelineConfigCompanion {
def apply[Query <: PipelineQuery, Candidate <: UniversalNoun[Any]](
scorer: Scorer[Query, Candidate]
): ScoringPipelineConfig[Query, Candidate] = new ScoringPipelineConfig[Query, Candidate] {
override val identifier: ScoringPipelineIdentifier = ScoringPipelineIdentifier(
s"ScoreAll${scorer.identifier.name}")
override val selectors: Seq[Selector[Query]] = Seq(InsertAppendResults(AllPipelines))
override val scorers: Seq[Scorer[Query, Candidate]] = Seq(scorer)
}
val gatesStep: PipelineStepIdentifier = PipelineStepIdentifier("Gates")
val selectorsStep: PipelineStepIdentifier = PipelineStepIdentifier("Selectors")
val preScoringFeatureHydrationPhase1Step: PipelineStepIdentifier =
PipelineStepIdentifier("PreScoringFeatureHydrationPhase1")
val preScoringFeatureHydrationPhase2Step: PipelineStepIdentifier =
PipelineStepIdentifier("PreScoringFeatureHydrationPhase2")
val scorersStep: PipelineStepIdentifier = PipelineStepIdentifier("Scorers")
val resultStep: PipelineStepIdentifier = PipelineStepIdentifier("Result")
/** All the Steps which are executed by a [[ScoringPipeline]] in the order in which they are run */
override val stepsInOrder: Seq[PipelineStepIdentifier] = Seq(
gatesStep,
selectorsStep,
preScoringFeatureHydrationPhase1Step,
preScoringFeatureHydrationPhase2Step,
scorersStep,
resultStep
)
}

View File

@ -1,51 +0,0 @@
package com.twitter.product_mixer.core.pipeline.scoring
import com.twitter.product_mixer.core.functional_component.scorer.ScoredCandidateResult
import com.twitter.product_mixer.core.model.common.UniversalNoun
import com.twitter.product_mixer.core.pipeline.PipelineResult
import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure
import com.twitter.product_mixer.core.service.candidate_feature_hydrator_executor.CandidateFeatureHydratorExecutorResult
import com.twitter.product_mixer.core.service.gate_executor.GateExecutorResult
import com.twitter.product_mixer.core.service.selector_executor.SelectorExecutorResult
/**
* The Results of every step during the ScoringPipeline process. The end result contains
* only the candidates that were actually scored (e.g, not dropped by a filter) with an updated,
* combined feature map of all features that were passed in with the candidate plus all features
* returned as part of scoring.
*/
case class ScoringPipelineResult[Candidate <: UniversalNoun[Any]](
gateResults: Option[GateExecutorResult],
selectorResults: Option[SelectorExecutorResult],
preScoringHydrationPhase1Result: Option[CandidateFeatureHydratorExecutorResult[Candidate]],
preScoringHydrationPhase2Result: Option[CandidateFeatureHydratorExecutorResult[Candidate]],
scorerResults: Option[CandidateFeatureHydratorExecutorResult[
Candidate
]],
failure: Option[PipelineFailure],
result: Option[Seq[ScoredCandidateResult[Candidate]]])
extends PipelineResult[Seq[ScoredCandidateResult[Candidate]]] {
override val resultSize: Int = result.map(_.size).getOrElse(0)
override def withFailure(
failure: PipelineFailure
): ScoringPipelineResult[Candidate] =
copy(failure = Some(failure))
override def withResult(
result: Seq[ScoredCandidateResult[Candidate]]
): ScoringPipelineResult[Candidate] =
copy(result = Some(result))
}
object ScoringPipelineResult {
def empty[Candidate <: UniversalNoun[Any]]: ScoringPipelineResult[Candidate] =
ScoringPipelineResult(
None,
None,
None,
None,
None,
None,
None
)
}

View File

@ -1,33 +0,0 @@
scala_library(
sources = [
"*.scala",
],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featuremap",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featuremap/asyncfeaturemap",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/decorator:decoration",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/presentation",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline:query",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
],
exports = [
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featuremap",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/feature/featuremap/asyncfeaturemap",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/decorator:decoration",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/presentation",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline:query",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service",
],
)

View File

@ -1,9 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.feature.featuremap.asyncfeaturemap.AsyncFeatureMap
trait HasAsyncFeatureMap[State] {
def asyncFeatureMap: AsyncFeatureMap
private[core] def addAsyncFeatureMap(newFeatureMap: AsyncFeatureMap): State
}

View File

@ -1,8 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.model.common.UniversalNoun
trait HasCandidates[Candidate <: UniversalNoun[Any], T] {
def candidates: Seq[Candidate]
def updateCandidates(newCandidates: Seq[Candidate]): T
}

View File

@ -1,11 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.functional_component.decorator.Decoration
import com.twitter.product_mixer.core.model.common.presentation.CandidateWithDetails
trait HasCandidatesWithDetails[T] {
def candidatesWithDetails: Seq[CandidateWithDetails]
def updateCandidatesWithDetails(newCandidates: Seq[CandidateWithDetails]): T
def updateDecorations(decoration: Seq[Decoration]): T
}

View File

@ -1,9 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.model.common.CandidateWithFeatures
import com.twitter.product_mixer.core.model.common.UniversalNoun
trait HasCandidatesWithFeatures[Candidate <: UniversalNoun[Any], T] {
def candidatesWithFeatures: Seq[CandidateWithFeatures[Candidate]]
def updateCandidatesWithFeatures(newCandidates: Seq[CandidateWithFeatures[Candidate]]): T
}

View File

@ -1,13 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.model.common.identifier.PipelineStepIdentifier
import com.twitter.product_mixer.core.service.ExecutorResult
import scala.collection.immutable.ListMap
trait HasExecutorResults[State] {
// We use a list map to maintain the insertion order
val executorResultsByPipelineStep: ListMap[PipelineStepIdentifier, ExecutorResult]
private[pipeline] def setExecutorResults(
newMap: ListMap[PipelineStepIdentifier, ExecutorResult]
): State
}

View File

@ -1,7 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.timelines.configapi.Params
trait HasParams {
def params: Params
}

View File

@ -1,8 +0,0 @@
package com.twitter.product_mixer.core.pipeline.state
import com.twitter.product_mixer.core.pipeline.PipelineQuery
trait HasQuery[Query <: PipelineQuery, T] {
def query: Query
def updateQuery(query: Query): T
}

Some files were not shown because too many files have changed in this diff Show More