mirror of
https://github.com/twitter/the-algorithm.git
synced 2024-11-16 08:29:21 +01:00
[docx] split commit for file 3600
Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
parent
9ebf058832
commit
ce29360463
Binary file not shown.
@ -1,87 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.predicate.ntab_caret_fatigue
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.predicate.FatiguePredicate
|
||||
import com.twitter.frigate.pushservice.predicate.CaretFeedbackHistoryFilter
|
||||
import com.twitter.frigate.pushservice.predicate.{
|
||||
TargetNtabCaretClickFatiguePredicate => CommonNtabCaretClickFatiguePredicate
|
||||
}
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.params.PushParams
|
||||
import com.twitter.frigate.thriftscala.NotificationDisplayLocation
|
||||
import com.twitter.frigate.thriftscala.{CommonRecommendationType => CRT}
|
||||
import com.twitter.hermit.predicate.NamedPredicate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
import com.twitter.notificationservice.thriftscala.CaretFeedbackDetails
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Future
|
||||
|
||||
object RecTypeNtabCaretClickFatiguePredicate {
|
||||
val defaultName = "RecTypeNtabCaretClickFatiguePredicateForCandidate"
|
||||
|
||||
private def candidateFatiguePredicate(
|
||||
genericTypeCategories: Seq[String],
|
||||
crts: Set[CRT]
|
||||
)(
|
||||
implicit stats: StatsReceiver
|
||||
): NamedPredicate[
|
||||
PushCandidate
|
||||
] = {
|
||||
val name = "f1TriggeredCRTBasedFatiguePredciate"
|
||||
val scopedStats = stats.scope(s"predicate_$name")
|
||||
Predicate
|
||||
.fromAsync { candidate: PushCandidate =>
|
||||
if (candidate.frigateNotification.notificationDisplayLocation == NotificationDisplayLocation.PushToMobileDevice) {
|
||||
if (candidate.target.params(PushParams.EnableFatigueNtabCaretClickingParam)) {
|
||||
NtabCaretClickContFnFatiguePredicate
|
||||
.ntabCaretClickContFnFatiguePredicates(
|
||||
filterHistory = FatiguePredicate.recTypesOnlyFilter(crts),
|
||||
filterCaretFeedbackHistory =
|
||||
CaretFeedbackHistoryFilter.caretFeedbackHistoryFilter(genericTypeCategories),
|
||||
filterInlineFeedbackHistory =
|
||||
NtabCaretClickFatigueUtils.feedbackModelFilterByCRT(crts)
|
||||
).apply(Seq(candidate))
|
||||
.map(_.headOption.getOrElse(false))
|
||||
} else Future.True
|
||||
} else {
|
||||
Future.True
|
||||
}
|
||||
}.withStats(scopedStats)
|
||||
.withName(name)
|
||||
}
|
||||
|
||||
def apply(
|
||||
genericTypeCategories: Seq[String],
|
||||
crts: Set[CRT],
|
||||
calculateFatiguePeriod: Seq[CaretFeedbackDetails] => Duration,
|
||||
useMostRecentDislikeTime: Boolean,
|
||||
name: String = defaultName
|
||||
)(
|
||||
implicit globalStats: StatsReceiver
|
||||
): NamedPredicate[PushCandidate] = {
|
||||
val scopedStats = globalStats.scope(name)
|
||||
val commonNtabCaretClickFatiguePredicate = CommonNtabCaretClickFatiguePredicate(
|
||||
filterCaretFeedbackHistory =
|
||||
CaretFeedbackHistoryFilter.caretFeedbackHistoryFilter(genericTypeCategories),
|
||||
filterHistory = FatiguePredicate.recTypesOnlyFilter(crts),
|
||||
calculateFatiguePeriod = calculateFatiguePeriod,
|
||||
useMostRecentDislikeTime = useMostRecentDislikeTime,
|
||||
name = name
|
||||
)(globalStats)
|
||||
|
||||
Predicate
|
||||
.fromAsync { candidate: PushCandidate =>
|
||||
if (candidate.frigateNotification.notificationDisplayLocation == NotificationDisplayLocation.PushToMobileDevice) {
|
||||
if (candidate.target.params(PushParams.EnableFatigueNtabCaretClickingParam)) {
|
||||
commonNtabCaretClickFatiguePredicate
|
||||
.apply(Seq(candidate.target))
|
||||
.map(_.headOption.getOrElse(false))
|
||||
} else Future.True
|
||||
} else {
|
||||
Future.True
|
||||
}
|
||||
}.andThen(candidateFatiguePredicate(genericTypeCategories, crts))
|
||||
.withStats(scopedStats)
|
||||
.withName(name)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,44 +0,0 @@
|
||||
package com.twitter.frigate.pushservice
|
||||
|
||||
import com.twitter.frigate.common.base.Candidate
|
||||
import com.twitter.frigate.common.base.SocialGraphServiceRelationshipMap
|
||||
import com.twitter.frigate.common.base.TweetAuthor
|
||||
import com.twitter.frigate.common.rec_types.RecTypes.isInNetworkTweetType
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
|
||||
package object predicate {
|
||||
implicit class CandidatesWithAuthorFollowPredicates(
|
||||
predicate: Predicate[
|
||||
PushCandidate with TweetAuthor with SocialGraphServiceRelationshipMap
|
||||
]) {
|
||||
def applyOnlyToAuthorBeingFollowPredicates: Predicate[Candidate] =
|
||||
predicate.optionalOn[Candidate](
|
||||
{
|
||||
case candidate: PushCandidate with TweetAuthor with SocialGraphServiceRelationshipMap
|
||||
if isInNetworkTweetType(candidate.commonRecType) =>
|
||||
Some(candidate)
|
||||
case _ =>
|
||||
None
|
||||
},
|
||||
missingResult = true
|
||||
)
|
||||
}
|
||||
|
||||
implicit class TweetCandidateWithTweetAuthor(
|
||||
predicate: Predicate[
|
||||
PushCandidate with TweetAuthor with SocialGraphServiceRelationshipMap
|
||||
]) {
|
||||
def applyOnlyToBasicTweetPredicates: Predicate[Candidate] =
|
||||
predicate.optionalOn[Candidate](
|
||||
{
|
||||
case candidate: PushCandidate with TweetAuthor with SocialGraphServiceRelationshipMap
|
||||
if isInNetworkTweetType(candidate.commonRecType) =>
|
||||
Some(candidate)
|
||||
case _ =>
|
||||
None
|
||||
},
|
||||
missingResult = true
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,27 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.predicate.quality_model_predicate
|
||||
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.util.Future
|
||||
|
||||
object ExplicitOONCFilterPredicate extends QualityPredicateBase {
|
||||
override lazy val name = "open_or_ntab_click_explicit_threshold"
|
||||
|
||||
override lazy val thresholdExtractor = (t: Target) =>
|
||||
Future.value(t.params(PushFeatureSwitchParams.QualityPredicateExplicitThresholdParam))
|
||||
|
||||
override def scoreExtractor = (candidate: PushCandidate) =>
|
||||
candidate.mrWeightedOpenOrNtabClickRankingProbability
|
||||
}
|
||||
|
||||
object WeightedOpenOrNtabClickQualityPredicate extends QualityPredicateBase {
|
||||
override lazy val name = "weighted_open_or_ntab_click_model"
|
||||
|
||||
override lazy val thresholdExtractor = (t: Target) => {
|
||||
Future.value(0.0)
|
||||
}
|
||||
|
||||
override def scoreExtractor =
|
||||
(candidate: PushCandidate) => candidate.mrWeightedOpenOrNtabClickFilteringProbability
|
||||
}
|
Binary file not shown.
@ -1,165 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.predicate.quality_model_predicate
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.pushservice.model.PushTypes
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.target.TargetScoringDetails
|
||||
import com.twitter.hermit.predicate.NamedPredicate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
import com.twitter.util.Future
|
||||
|
||||
object PDauCohort extends Enumeration {
|
||||
type PDauCohort = Value
|
||||
|
||||
val cohort1 = Value
|
||||
val cohort2 = Value
|
||||
val cohort3 = Value
|
||||
val cohort4 = Value
|
||||
val cohort5 = Value
|
||||
val cohort6 = Value
|
||||
}
|
||||
|
||||
object PDauCohortUtil {
|
||||
|
||||
case class DauThreshold(
|
||||
threshold1: Double,
|
||||
threshold2: Double,
|
||||
threshold3: Double,
|
||||
threshold4: Double,
|
||||
threshold5: Double)
|
||||
|
||||
val defaultDAUProb = 0.0
|
||||
|
||||
val dauProbThresholds = DauThreshold(
|
||||
threshold1 = 0.05,
|
||||
threshold2 = 0.14,
|
||||
threshold3 = 0.33,
|
||||
threshold4 = 0.7,
|
||||
threshold5 = 0.959
|
||||
)
|
||||
|
||||
val finerThresholdMap =
|
||||
Map(
|
||||
PDauCohort.cohort2 -> List(0.05, 0.0539, 0.0563, 0.0600, 0.0681, 0.0733, 0.0800, 0.0849,
|
||||
0.0912, 0.0975, 0.1032, 0.1092, 0.1134, 0.1191, 0.1252, 0.1324, 0.14),
|
||||
PDauCohort.cohort3 -> List(0.14, 0.1489, 0.1544, 0.1625, 0.1704, 0.1797, 0.1905, 0.2001,
|
||||
0.2120, 0.2248, 0.2363, 0.2500, 0.2650, 0.2801, 0.2958, 0.3119, 0.33),
|
||||
PDauCohort.cohort4 -> List(0.33, 0.3484, 0.3686, 0.3893, 0.4126, 0.4350, 0.4603, 0.4856,
|
||||
0.5092, 0.5348, 0.5602, 0.5850, 0.6087, 0.6319, 0.6548, 0.6779, 0.7),
|
||||
PDauCohort.cohort5 -> List(0.7, 0.7295, 0.7581, 0.7831, 0.8049, 0.8251, 0.8444, 0.8612,
|
||||
0.8786, 0.8936, 0.9043, 0.9175, 0.9290, 0.9383, 0.9498, 0.9587, 0.959)
|
||||
)
|
||||
|
||||
def getBucket(targetUser: PushTypes.Target, doImpression: Boolean) = {
|
||||
implicit val stats = targetUser.stats.scope("PDauCohortUtil")
|
||||
if (doImpression) targetUser.getBucket _ else targetUser.getBucketWithoutImpression _
|
||||
}
|
||||
|
||||
def threshold1(targetUser: PushTypes.Target): Double = dauProbThresholds.threshold1
|
||||
|
||||
def threshold2(targetUser: PushTypes.Target): Double = dauProbThresholds.threshold2
|
||||
|
||||
def threshold3(targetUser: PushTypes.Target): Double = dauProbThresholds.threshold3
|
||||
|
||||
def threshold4(targetUser: PushTypes.Target): Double = dauProbThresholds.threshold4
|
||||
|
||||
def threshold5(targetUser: PushTypes.Target): Double = dauProbThresholds.threshold5
|
||||
|
||||
def thresholdForCohort(targetUser: PushTypes.Target, dauCohort: Int): Double = {
|
||||
if (dauCohort == 0) 0.0
|
||||
else if (dauCohort == 1) threshold1(targetUser)
|
||||
else if (dauCohort == 2) threshold2(targetUser)
|
||||
else if (dauCohort == 3) threshold3(targetUser)
|
||||
else if (dauCohort == 4) threshold4(targetUser)
|
||||
else if (dauCohort == 5) threshold5(targetUser)
|
||||
else 1.0
|
||||
}
|
||||
|
||||
def getPDauCohort(dauProbability: Double, thresholds: DauThreshold): PDauCohort.Value = {
|
||||
dauProbability match {
|
||||
case dauProb if dauProb >= 0.0 && dauProb < thresholds.threshold1 => PDauCohort.cohort1
|
||||
case dauProb if dauProb >= thresholds.threshold1 && dauProb < thresholds.threshold2 =>
|
||||
PDauCohort.cohort2
|
||||
case dauProb if dauProb >= thresholds.threshold2 && dauProb < thresholds.threshold3 =>
|
||||
PDauCohort.cohort3
|
||||
case dauProb if dauProb >= thresholds.threshold3 && dauProb < thresholds.threshold4 =>
|
||||
PDauCohort.cohort4
|
||||
case dauProb if dauProb >= thresholds.threshold4 && dauProb < thresholds.threshold5 =>
|
||||
PDauCohort.cohort5
|
||||
case dauProb if dauProb >= thresholds.threshold5 && dauProb <= 1.0 => PDauCohort.cohort6
|
||||
}
|
||||
}
|
||||
|
||||
def getDauProb(target: TargetScoringDetails): Future[Double] = {
|
||||
target.dauProbability.map { dauProb =>
|
||||
dauProb.map(_.probability).getOrElse(defaultDAUProb)
|
||||
}
|
||||
}
|
||||
|
||||
def getPDauCohort(target: TargetScoringDetails): Future[PDauCohort.Value] = {
|
||||
getDauProb(target).map { getPDauCohort(_, dauProbThresholds) }
|
||||
}
|
||||
|
||||
def getPDauCohortWithPDau(target: TargetScoringDetails): Future[(PDauCohort.Value, Double)] = {
|
||||
getDauProb(target).map { prob =>
|
||||
(getPDauCohort(prob, dauProbThresholds), prob)
|
||||
}
|
||||
}
|
||||
|
||||
def updateStats(
|
||||
target: PushTypes.Target,
|
||||
modelName: String,
|
||||
predicateResult: Boolean
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver
|
||||
): Unit = {
|
||||
val dauCohortOp = getPDauCohort(target)
|
||||
dauCohortOp.map { dauCohort =>
|
||||
val cohortStats = statsReceiver.scope(modelName).scope(dauCohort.toString)
|
||||
cohortStats.counter(s"filter_$predicateResult").incr()
|
||||
}
|
||||
if (target.isNewSignup) {
|
||||
val newUserModelStats = statsReceiver.scope(modelName)
|
||||
newUserModelStats.counter(s"new_user_filter_$predicateResult").incr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait QualityPredicateBase {
|
||||
def name: String
|
||||
def thresholdExtractor: Target => Future[Double]
|
||||
def scoreExtractor: PushCandidate => Future[Option[Double]]
|
||||
def isPredicateEnabled: PushCandidate => Future[Boolean] = _ => Future.True
|
||||
def comparator: (Double, Double) => Boolean =
|
||||
(score: Double, threshold: Double) => score >= threshold
|
||||
def updateCustomStats(
|
||||
candidate: PushCandidate,
|
||||
score: Double,
|
||||
threshold: Double,
|
||||
result: Boolean
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver
|
||||
): Unit = {}
|
||||
|
||||
def apply()(implicit statsReceiver: StatsReceiver): NamedPredicate[PushCandidate] = {
|
||||
Predicate
|
||||
.fromAsync { candidate: PushCandidate =>
|
||||
isPredicateEnabled(candidate).flatMap {
|
||||
case true =>
|
||||
scoreExtractor(candidate).flatMap { scoreOpt =>
|
||||
thresholdExtractor(candidate.target).map { threshold =>
|
||||
val score = scoreOpt.getOrElse(0.0)
|
||||
val result = comparator(score, threshold)
|
||||
PDauCohortUtil.updateStats(candidate.target, name, result)
|
||||
updateCustomStats(candidate, score, threshold, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
case _ => Future.True
|
||||
}
|
||||
}
|
||||
.withStats(statsReceiver.scope(s"predicate_$name"))
|
||||
.withName(name)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,21 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.predicate.quality_model_predicate
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.params.QualityPredicateEnum
|
||||
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||
import com.twitter.hermit.predicate.NamedPredicate
|
||||
|
||||
object QualityPredicateMap {
|
||||
|
||||
def apply(
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver
|
||||
): Map[QualityPredicateEnum.Value, NamedPredicate[PushCandidate]] = {
|
||||
Map(
|
||||
QualityPredicateEnum.WeightedOpenOrNtabClick -> WeightedOpenOrNtabClickQualityPredicate(),
|
||||
QualityPredicateEnum.ExplicitOpenOrNtabClickFilter -> ExplicitOONCFilterPredicate(),
|
||||
QualityPredicateEnum.AlwaysTrue -> PredicatesForCandidate.alwaysTruePushCandidatePredicate,
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,54 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
|
||||
/**
|
||||
* This Ranker re-ranks MR candidates, boosting input CRTs.
|
||||
* Relative ranking between input CRTs and rest of the candidates doesn't change
|
||||
*
|
||||
* Ex: T: Tweet candidate, F: input CRT candidatess
|
||||
*
|
||||
* T3, F2, T1, T2, F1 => F2, F1, T3, T1, T2
|
||||
*/
|
||||
case class CRTBoostRanker(statsReceiver: StatsReceiver) {
|
||||
|
||||
private val recsToBoostStat = statsReceiver.stat("recs_to_boost")
|
||||
private val otherRecsStat = statsReceiver.stat("other_recs")
|
||||
|
||||
private def boostCrtToTop(
|
||||
inputCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
crtToBoost: CommonRecommendationType
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
val (upRankedCandidates, otherCandidates) =
|
||||
inputCandidates.partition(_.candidate.commonRecType == crtToBoost)
|
||||
recsToBoostStat.add(upRankedCandidates.size)
|
||||
otherRecsStat.add(otherCandidates.size)
|
||||
upRankedCandidates ++ otherCandidates
|
||||
}
|
||||
|
||||
final def boostCrtsToTop(
|
||||
inputCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
crtsToBoost: Seq[CommonRecommendationType]
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
crtsToBoost.headOption match {
|
||||
case Some(crt) =>
|
||||
val upRankedCandidates = boostCrtToTop(inputCandidates, crt)
|
||||
boostCrtsToTop(upRankedCandidates, crtsToBoost.tail)
|
||||
case None => inputCandidates
|
||||
}
|
||||
}
|
||||
|
||||
final def boostCrtsToTopStableOrder(
|
||||
inputCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
crtsToBoost: Seq[CommonRecommendationType]
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
val crtsToBoostSet = crtsToBoost.toSet
|
||||
val (upRankedCandidates, otherCandidates) = inputCandidates.partition(candidateDetail =>
|
||||
crtsToBoostSet.contains(candidateDetail.candidate.commonRecType))
|
||||
|
||||
upRankedCandidates ++ otherCandidates
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,45 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
|
||||
/**
|
||||
* This Ranker re-ranks MR candidates, down ranks input CRTs.
|
||||
* Relative ranking between input CRTs and rest of the candidates doesn't change
|
||||
*
|
||||
* Ex: T: Tweet candidate, F: input CRT candidates
|
||||
*
|
||||
* T3, F2, T1, T2, F1 => T3, T1, T2, F2, F1
|
||||
*/
|
||||
case class CRTDownRanker(statsReceiver: StatsReceiver) {
|
||||
|
||||
private val recsToDownRankStat = statsReceiver.stat("recs_to_down_rank")
|
||||
private val otherRecsStat = statsReceiver.stat("other_recs")
|
||||
private val downRankerRequests = statsReceiver.counter("down_ranker_requests")
|
||||
|
||||
private def downRank(
|
||||
inputCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
crtToDownRank: CommonRecommendationType
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
downRankerRequests.incr()
|
||||
val (downRankedCandidates, otherCandidates) =
|
||||
inputCandidates.partition(_.candidate.commonRecType == crtToDownRank)
|
||||
recsToDownRankStat.add(downRankedCandidates.size)
|
||||
otherRecsStat.add(otherCandidates.size)
|
||||
otherCandidates ++ downRankedCandidates
|
||||
}
|
||||
|
||||
final def downRank(
|
||||
inputCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
crtsToDownRank: Seq[CommonRecommendationType]
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
crtsToDownRank.headOption match {
|
||||
case Some(crt) =>
|
||||
val downRankedCandidates = downRank(inputCandidates, crt)
|
||||
downRank(downRankedCandidates, crtsToDownRank.tail)
|
||||
case None => inputCandidates
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,45 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.TweetCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
class LoggedOutRanker(tweetyPieStore: ReadableStore[Long, TweetyPieResult], stats: StatsReceiver) {
|
||||
private val statsReceiver = stats.scope(this.getClass.getSimpleName)
|
||||
private val rankedCandidates = statsReceiver.counter("ranked_candidates_count")
|
||||
|
||||
def rank(
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val tweetIds = candidates.map { cand => cand.candidate.asInstanceOf[TweetCandidate].tweetId }
|
||||
val results = tweetyPieStore.multiGet(tweetIds.toSet).values.toSeq
|
||||
val futureOfResults = Future.traverseSequentially(results)(r => r)
|
||||
val tweetsFut = futureOfResults.map { tweetyPieResults =>
|
||||
tweetyPieResults.map(_.map(_.tweet))
|
||||
}
|
||||
val sortedTweetsFuture = tweetsFut.map { tweets =>
|
||||
tweets
|
||||
.map { tweet =>
|
||||
if (tweet.isDefined && tweet.get.counts.isDefined) {
|
||||
tweet.get.id -> tweet.get.counts.get.favoriteCount.getOrElse(0L)
|
||||
} else {
|
||||
0 -> 0L
|
||||
}
|
||||
}.sortBy(_._2)(Ordering[Long].reverse)
|
||||
}
|
||||
val finalCandidates = sortedTweetsFuture.map { sortedTweets =>
|
||||
sortedTweets
|
||||
.map { tweet =>
|
||||
candidates.find(_.candidate.asInstanceOf[TweetCandidate].tweetId == tweet._1).orNull
|
||||
}.filter { cand => cand != null }
|
||||
}
|
||||
finalCandidates.map { fc =>
|
||||
rankedCandidates.incr(fc.size)
|
||||
}
|
||||
finalCandidates
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,204 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.util.Future
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.pushservice.params.MrQualityUprankingPartialTypeEnum
|
||||
import com.twitter.frigate.common.base.TweetCandidate
|
||||
import com.twitter.frigate.common.rec_types.RecTypes
|
||||
import com.twitter.frigate.pushservice.params.PushConstants.OoncQualityCombinedScore
|
||||
|
||||
object ModelBasedRanker {
|
||||
|
||||
def rankBySpecifiedScore(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
scoreExtractor: PushCandidate => Future[Option[Double]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
val scoredCandidatesFutures = candidatesDetails.map { cand =>
|
||||
scoreExtractor(cand.candidate).map { scoreOp => (cand, scoreOp.getOrElse(0.0)) }
|
||||
}
|
||||
|
||||
Future.collect(scoredCandidatesFutures).map { scores =>
|
||||
val sorted = scores.sortBy { candidateDetails => -1 * candidateDetails._2 }
|
||||
sorted.map(_._1)
|
||||
}
|
||||
}
|
||||
|
||||
def populatePredictionScoreStats(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
scoreExtractor: PushCandidate => Future[Option[Double]],
|
||||
predictionScoreStats: StatsReceiver
|
||||
): Unit = {
|
||||
val scoreScaleFactorForStat = 10000
|
||||
val statName = "prediction_scores"
|
||||
candidatesDetails.map {
|
||||
case CandidateDetails(candidate, source) =>
|
||||
val crt = candidate.commonRecType
|
||||
scoreExtractor(candidate).map { scoreOp =>
|
||||
val scaledScore = (scoreOp.getOrElse(0.0) * scoreScaleFactorForStat).toFloat
|
||||
predictionScoreStats.scope("all_candidates").stat(statName).add(scaledScore)
|
||||
predictionScoreStats.scope(crt.toString()).stat(statName).add(scaledScore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def populateMrWeightedOpenOrNtabClickScoreStats(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
predictionScoreStats: StatsReceiver
|
||||
): Unit = {
|
||||
populatePredictionScoreStats(
|
||||
candidatesDetails,
|
||||
candidate => candidate.mrWeightedOpenOrNtabClickRankingProbability,
|
||||
predictionScoreStats
|
||||
)
|
||||
}
|
||||
|
||||
def populateMrQualityUprankingScoreStats(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
predictionScoreStats: StatsReceiver
|
||||
): Unit = {
|
||||
populatePredictionScoreStats(
|
||||
candidatesDetails,
|
||||
candidate => candidate.mrQualityUprankingProbability,
|
||||
predictionScoreStats
|
||||
)
|
||||
}
|
||||
|
||||
def rankByMrWeightedOpenOrNtabClickScore(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
rankBySpecifiedScore(
|
||||
candidatesDetails,
|
||||
candidate => candidate.mrWeightedOpenOrNtabClickRankingProbability
|
||||
)
|
||||
}
|
||||
|
||||
def transformSigmoid(
|
||||
score: Double,
|
||||
weight: Double = 1.0,
|
||||
bias: Double = 0.0
|
||||
): Double = {
|
||||
val base = -1.0 * (weight * score + bias)
|
||||
val cappedBase = math.max(math.min(base, 100.0), -100.0)
|
||||
1.0 / (1.0 + math.exp(cappedBase))
|
||||
}
|
||||
|
||||
def transformLinear(
|
||||
score: Double,
|
||||
bar: Double = 1.0
|
||||
): Double = {
|
||||
val positiveBar = math.abs(bar)
|
||||
val cappedScore = math.max(math.min(score, positiveBar), -1.0 * positiveBar)
|
||||
cappedScore / positiveBar
|
||||
}
|
||||
|
||||
def transformIdentity(
|
||||
score: Double
|
||||
): Double = score
|
||||
|
||||
def rankByQualityOoncCombinedScore(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
qualityScoreTransform: Double => Double,
|
||||
qualityScoreBoost: Double = 1.0
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
rankBySpecifiedScore(
|
||||
candidatesDetails,
|
||||
candidate => {
|
||||
val ooncScoreFutOpt: Future[Option[Double]] =
|
||||
candidate.mrWeightedOpenOrNtabClickRankingProbability
|
||||
val qualityScoreFutOpt: Future[Option[Double]] =
|
||||
candidate.mrQualityUprankingProbability
|
||||
Future
|
||||
.join(
|
||||
ooncScoreFutOpt,
|
||||
qualityScoreFutOpt
|
||||
).map {
|
||||
case (Some(ooncScore), Some(qualityScore)) =>
|
||||
val transformedQualityScore = qualityScoreTransform(qualityScore)
|
||||
val combinedScore = ooncScore * (1.0 + qualityScoreBoost * transformedQualityScore)
|
||||
candidate
|
||||
.cacheExternalScore(OoncQualityCombinedScore, Future.value(Some(combinedScore)))
|
||||
Some(combinedScore)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def rerankByProducerQualityOoncCombinedScore(
|
||||
candidateDetails: Seq[CandidateDetails[PushCandidate]]
|
||||
)(
|
||||
implicit stat: StatsReceiver
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val scopedStat = stat.scope("producer_quality_reranking")
|
||||
val oonCandidates = candidateDetails.filter {
|
||||
case CandidateDetails(pushCandidate: PushCandidate, _) =>
|
||||
tweetCandidateSelector(pushCandidate, MrQualityUprankingPartialTypeEnum.Oon)
|
||||
}
|
||||
|
||||
val rankedOonCandidatesFut = rankBySpecifiedScore(
|
||||
oonCandidates,
|
||||
candidate => {
|
||||
val baseScoreFutureOpt: Future[Option[Double]] = {
|
||||
val qualityCombinedScoreFutureOpt =
|
||||
candidate.getExternalCachedScoreByName(OoncQualityCombinedScore)
|
||||
val ooncScoreFutureOpt = candidate.mrWeightedOpenOrNtabClickRankingProbability
|
||||
Future.join(qualityCombinedScoreFutureOpt, ooncScoreFutureOpt).map {
|
||||
case (Some(qualityCombinedScore), _) =>
|
||||
scopedStat.counter("quality_combined_score").incr()
|
||||
Some(qualityCombinedScore)
|
||||
case (_, ooncScoreOpt) =>
|
||||
scopedStat.counter("oonc_score").incr()
|
||||
ooncScoreOpt
|
||||
}
|
||||
}
|
||||
baseScoreFutureOpt.map {
|
||||
case Some(baseScore) =>
|
||||
val boostRatio = candidate.mrProducerQualityUprankingBoost.getOrElse(1.0)
|
||||
if (boostRatio > 1.0) scopedStat.counter("author_uprank").incr()
|
||||
else if (boostRatio < 1.0) scopedStat.counter("author_downrank").incr()
|
||||
else scopedStat.counter("author_noboost").incr()
|
||||
Some(baseScore * boostRatio)
|
||||
case _ =>
|
||||
scopedStat.counter("empty_score").incr()
|
||||
None
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
rankedOonCandidatesFut.map { rankedOonCandidates =>
|
||||
val sortedOonCandidateIterator = rankedOonCandidates.toIterator
|
||||
candidateDetails.map { ooncRankedCandidate =>
|
||||
val isOon = tweetCandidateSelector(
|
||||
ooncRankedCandidate.candidate,
|
||||
MrQualityUprankingPartialTypeEnum.Oon)
|
||||
|
||||
if (sortedOonCandidateIterator.hasNext && isOon)
|
||||
sortedOonCandidateIterator.next()
|
||||
else ooncRankedCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def tweetCandidateSelector(
|
||||
pushCandidate: PushCandidate,
|
||||
selectedCandidateType: MrQualityUprankingPartialTypeEnum.Value
|
||||
): Boolean = {
|
||||
pushCandidate match {
|
||||
case candidate: PushCandidate with TweetCandidate =>
|
||||
selectedCandidateType match {
|
||||
case MrQualityUprankingPartialTypeEnum.Oon =>
|
||||
val crt = candidate.commonRecType
|
||||
RecTypes.isOutOfNetworkTweetRecType(crt) || RecTypes.outOfNetworkTopicTweetTypes
|
||||
.contains(crt)
|
||||
case _ => true
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,31 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.Ranker
|
||||
import com.twitter.util.Future
|
||||
|
||||
trait PushserviceRanker[T, C] extends Ranker[T, C] {
|
||||
|
||||
/**
|
||||
* Initial Ranking of input candidates
|
||||
*/
|
||||
def initialRank(target: T, candidates: Seq[CandidateDetails[C]]): Future[Seq[CandidateDetails[C]]]
|
||||
|
||||
/**
|
||||
* Re-ranks input ranked candidates. Useful when a subset of candidates are ranked
|
||||
* by a different logic, while preserving the initial ranking for the rest
|
||||
*/
|
||||
def reRank(
|
||||
target: T,
|
||||
rankedCandidates: Seq[CandidateDetails[C]]
|
||||
): Future[Seq[CandidateDetails[C]]]
|
||||
|
||||
/**
|
||||
* Final ranking that does Initial + Rerank
|
||||
*/
|
||||
override final def rank(target: T, candidates: Seq[CandidateDetails[C]]): (
|
||||
Future[Seq[CandidateDetails[C]]]
|
||||
) = {
|
||||
initialRank(target, candidates).flatMap { rankedCandidates => reRank(target, rankedCandidates) }
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,139 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
import com.twitter.contentrecommender.thriftscala.LightRankingCandidate
|
||||
import com.twitter.contentrecommender.thriftscala.LightRankingFeatureHydrationContext
|
||||
import com.twitter.contentrecommender.thriftscala.MagicRecsFeatureHydrationContext
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.RandomRanker
|
||||
import com.twitter.frigate.common.base.Ranker
|
||||
import com.twitter.frigate.common.base.TweetAuthor
|
||||
import com.twitter.frigate.common.base.TweetCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.params.PushConstants
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.frigate.pushservice.params.PushParams
|
||||
import com.twitter.ml.featurestore.lib.UserId
|
||||
import com.twitter.nrel.lightranker.MagicRecsServeDataRecordLightRanker
|
||||
import com.twitter.util.Future
|
||||
|
||||
class RFPHLightRanker(
|
||||
lightRanker: MagicRecsServeDataRecordLightRanker,
|
||||
stats: StatsReceiver)
|
||||
extends Ranker[Target, PushCandidate] {
|
||||
|
||||
private val statsReceiver = stats.scope(this.getClass.getSimpleName)
|
||||
|
||||
private val lightRankerCandidateCounter = statsReceiver.counter("light_ranker_candidate_count")
|
||||
private val lightRankerRequestCounter = statsReceiver.counter("light_ranker_request_count")
|
||||
private val lightRankingStats: StatsReceiver = statsReceiver.scope("light_ranking")
|
||||
private val restrictLightRankingCounter: Counter =
|
||||
lightRankingStats.counter("restrict_light_ranking")
|
||||
private val selectedLightRankerScribedTargetCandidateCountStats: Stat =
|
||||
lightRankingStats.stat("selected_light_ranker_scribed_target_candidate_count")
|
||||
private val selectedLightRankerScribedCandidatesStats: Stat =
|
||||
lightRankingStats.stat("selected_light_ranker_scribed_candidates")
|
||||
private val lightRankingRandomBaselineStats: StatsReceiver =
|
||||
statsReceiver.scope("light_ranking_random_baseline")
|
||||
|
||||
override def rank(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val enableLightRanker = target.params(PushFeatureSwitchParams.EnableLightRankingParam)
|
||||
val restrictLightRanker = target.params(PushParams.RestrictLightRankingParam)
|
||||
val lightRankerSelectionThreshold =
|
||||
target.params(PushFeatureSwitchParams.LightRankingNumberOfCandidatesParam)
|
||||
val randomRanker = RandomRanker[Target, PushCandidate]()(lightRankingRandomBaselineStats)
|
||||
|
||||
if (enableLightRanker && candidates.length > lightRankerSelectionThreshold && !target.scribeFeatureForRequestScribe) {
|
||||
val (tweetCandidates, nonTweetCandidates) =
|
||||
candidates.partition {
|
||||
case CandidateDetails(pushCandidate: PushCandidate with TweetCandidate, source) => true
|
||||
case _ => false
|
||||
}
|
||||
val lightRankerSelectedTweetCandidatesFut = {
|
||||
if (restrictLightRanker) {
|
||||
restrictLightRankingCounter.incr()
|
||||
lightRankThenTake(
|
||||
target,
|
||||
tweetCandidates
|
||||
.asInstanceOf[Seq[CandidateDetails[PushCandidate with TweetCandidate]]],
|
||||
PushConstants.RestrictLightRankingCandidatesThreshold
|
||||
)
|
||||
} else if (target.params(PushFeatureSwitchParams.EnableRandomBaselineLightRankingParam)) {
|
||||
randomRanker.rank(target, tweetCandidates).map { randomLightRankerCands =>
|
||||
randomLightRankerCands.take(lightRankerSelectionThreshold)
|
||||
}
|
||||
} else {
|
||||
lightRankThenTake(
|
||||
target,
|
||||
tweetCandidates
|
||||
.asInstanceOf[Seq[CandidateDetails[PushCandidate with TweetCandidate]]],
|
||||
lightRankerSelectionThreshold
|
||||
)
|
||||
}
|
||||
}
|
||||
lightRankerSelectedTweetCandidatesFut.map { returnedTweetCandidates =>
|
||||
nonTweetCandidates ++ returnedTweetCandidates
|
||||
}
|
||||
} else if (target.scribeFeatureForRequestScribe) {
|
||||
val downSampleRate: Double =
|
||||
if (target.params(PushParams.DownSampleLightRankingScribeCandidatesParam))
|
||||
PushConstants.DownSampleLightRankingScribeCandidatesRate
|
||||
else target.params(PushFeatureSwitchParams.LightRankingScribeCandidatesDownSamplingParam)
|
||||
val selectedCandidateCounter: Int = math.ceil(candidates.size * downSampleRate).toInt
|
||||
selectedLightRankerScribedTargetCandidateCountStats.add(selectedCandidateCounter.toFloat)
|
||||
|
||||
randomRanker.rank(target, candidates).map { randomLightRankerCands =>
|
||||
val selectedCandidates = randomLightRankerCands.take(selectedCandidateCounter)
|
||||
selectedLightRankerScribedCandidatesStats.add(selectedCandidates.size.toFloat)
|
||||
selectedCandidates
|
||||
}
|
||||
} else Future.value(candidates)
|
||||
}
|
||||
|
||||
private def lightRankThenTake(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate with TweetCandidate]],
|
||||
numOfCandidates: Int
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
lightRankerCandidateCounter.incr(candidates.length)
|
||||
lightRankerRequestCounter.incr()
|
||||
val lightRankerCandidates: Seq[LightRankingCandidate] = candidates.map {
|
||||
case CandidateDetails(tweetCandidate, _) =>
|
||||
val tweetAuthor = tweetCandidate match {
|
||||
case t: TweetCandidate with TweetAuthor => t.authorId
|
||||
case _ => None
|
||||
}
|
||||
val hydrationContext: LightRankingFeatureHydrationContext =
|
||||
LightRankingFeatureHydrationContext.MagicRecsHydrationContext(
|
||||
MagicRecsFeatureHydrationContext(
|
||||
tweetAuthor = tweetAuthor,
|
||||
pushString = tweetCandidate.getPushCopy.flatMap(_.pushStringGroup).map(_.toString))
|
||||
)
|
||||
LightRankingCandidate(
|
||||
tweetId = tweetCandidate.tweetId,
|
||||
hydrationContext = Some(hydrationContext)
|
||||
)
|
||||
}
|
||||
val modelName = target.params(PushFeatureSwitchParams.LightRankingModelTypeParam)
|
||||
val lightRankedCandidatesFut = {
|
||||
lightRanker
|
||||
.rank(UserId(target.targetId), lightRankerCandidates, modelName)
|
||||
}
|
||||
|
||||
lightRankedCandidatesFut.map { lightRankedCandidates =>
|
||||
val lrScoreMap = lightRankedCandidates.map { lrCand =>
|
||||
lrCand.tweetId -> lrCand.score
|
||||
}.toMap
|
||||
val candScoreMap: Seq[Option[Double]] = candidates.map { candidateDetails =>
|
||||
lrScoreMap.get(candidateDetails.candidate.tweetId)
|
||||
}
|
||||
sortCandidatesByScore(candidates, candScoreMap)
|
||||
.take(numOfCandidates)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,297 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.Ranker
|
||||
import com.twitter.frigate.common.rec_types.RecTypes
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.ml.HealthFeatureGetter
|
||||
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||
import com.twitter.frigate.pushservice.params.MrQualityUprankingPartialTypeEnum
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.frigate.pushservice.params.PushMLModel
|
||||
import com.twitter.frigate.pushservice.params.PushModelName
|
||||
import com.twitter.frigate.pushservice.params.PushParams
|
||||
import com.twitter.frigate.pushservice.util.MediaAnnotationsUtil.updateMediaCategoryStats
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.frigate.pushservice.params.MrQualityUprankingTransformTypeEnum
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.frigate.thriftscala.UserMediaRepresentation
|
||||
import com.twitter.hss.api.thriftscala.UserHealthSignalResponse
|
||||
|
||||
class RFPHRanker(
|
||||
randomRanker: Ranker[Target, PushCandidate],
|
||||
weightedOpenOrNtabClickModelScorer: PushMLModelScorer,
|
||||
subscriptionCreatorRanker: SubscriptionCreatorRanker,
|
||||
userHealthSignalStore: ReadableStore[Long, UserHealthSignalResponse],
|
||||
producerMediaRepresentationStore: ReadableStore[Long, UserMediaRepresentation],
|
||||
stats: StatsReceiver)
|
||||
extends PushserviceRanker[Target, PushCandidate] {
|
||||
|
||||
private val statsReceiver = stats.scope(this.getClass.getSimpleName)
|
||||
|
||||
private val boostCRTsRanker = CRTBoostRanker(statsReceiver.scope("boost_desired_crts"))
|
||||
private val crtDownRanker = CRTDownRanker(statsReceiver.scope("down_rank_desired_crts"))
|
||||
|
||||
private val crtsToDownRank = statsReceiver.stat("crts_to_downrank")
|
||||
private val crtsToUprank = statsReceiver.stat("crts_to_uprank")
|
||||
|
||||
private val randomRankingCounter = stats.counter("randomRanking")
|
||||
private val mlRankingCounter = stats.counter("mlRanking")
|
||||
private val disableAllRelevanceCounter = stats.counter("disableAllRelevance")
|
||||
private val disableHeavyRankingCounter = stats.counter("disableHeavyRanking")
|
||||
|
||||
private val heavyRankerCandidateCounter = stats.counter("heavy_ranker_candidate_count")
|
||||
private val heavyRankerScoreStats = statsReceiver.scope("heavy_ranker_prediction_scores")
|
||||
|
||||
private val producerUprankingCounter = statsReceiver.counter("producer_quality_upranking")
|
||||
private val producerBoostedCounter = statsReceiver.counter("producer_boosted_candidates")
|
||||
private val producerDownboostedCounter = statsReceiver.counter("producer_downboosted_candidates")
|
||||
|
||||
override def initialRank(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
heavyRankerCandidateCounter.incr(candidates.size)
|
||||
|
||||
updateMediaCategoryStats(candidates)(stats)
|
||||
target.targetUserState
|
||||
.flatMap { targetUserState =>
|
||||
val useRandomRanking = target.skipMlRanker || target.params(
|
||||
PushParams.UseRandomRankingParam
|
||||
)
|
||||
|
||||
if (useRandomRanking) {
|
||||
randomRankingCounter.incr()
|
||||
randomRanker.rank(target, candidates)
|
||||
} else if (target.params(PushParams.DisableAllRelevanceParam)) {
|
||||
disableAllRelevanceCounter.incr()
|
||||
Future.value(candidates)
|
||||
} else if (target.params(PushParams.DisableHeavyRankingParam) || target.params(
|
||||
PushFeatureSwitchParams.DisableHeavyRankingModelFSParam)) {
|
||||
disableHeavyRankingCounter.incr()
|
||||
Future.value(candidates)
|
||||
} else {
|
||||
mlRankingCounter.incr()
|
||||
|
||||
val scoredCandidatesFut = scoring(target, candidates)
|
||||
|
||||
target.rankingModelParam.map { rankingModelParam =>
|
||||
val modelName = PushModelName(
|
||||
PushMLModel.WeightedOpenOrNtabClickProbability,
|
||||
target.params(rankingModelParam)).toString
|
||||
ModelBasedRanker.populateMrWeightedOpenOrNtabClickScoreStats(
|
||||
candidates,
|
||||
heavyRankerScoreStats.scope(modelName)
|
||||
)
|
||||
}
|
||||
|
||||
if (target.params(
|
||||
PushFeatureSwitchParams.EnableQualityUprankingCrtScoreStatsForHeavyRankingParam)) {
|
||||
val modelName = PushModelName(
|
||||
PushMLModel.FilteringProbability,
|
||||
target.params(PushFeatureSwitchParams.QualityUprankingModelTypeParam)
|
||||
).toString
|
||||
ModelBasedRanker.populateMrQualityUprankingScoreStats(
|
||||
candidates,
|
||||
heavyRankerScoreStats.scope(modelName)
|
||||
)
|
||||
}
|
||||
|
||||
val ooncRankedCandidatesFut =
|
||||
scoredCandidatesFut.flatMap(ModelBasedRanker.rankByMrWeightedOpenOrNtabClickScore)
|
||||
|
||||
val qualityUprankedCandidatesFut =
|
||||
if (target.params(PushFeatureSwitchParams.EnableQualityUprankingForHeavyRankingParam)) {
|
||||
ooncRankedCandidatesFut.flatMap { ooncRankedCandidates =>
|
||||
val transformFunc: Double => Double =
|
||||
target.params(PushFeatureSwitchParams.QualityUprankingTransformTypeParam) match {
|
||||
case MrQualityUprankingTransformTypeEnum.Linear =>
|
||||
ModelBasedRanker.transformLinear(
|
||||
_,
|
||||
bar = target.params(
|
||||
PushFeatureSwitchParams.QualityUprankingLinearBarForHeavyRankingParam))
|
||||
case MrQualityUprankingTransformTypeEnum.Sigmoid =>
|
||||
ModelBasedRanker.transformSigmoid(
|
||||
_,
|
||||
weight = target.params(
|
||||
PushFeatureSwitchParams.QualityUprankingSigmoidWeightForHeavyRankingParam),
|
||||
bias = target.params(
|
||||
PushFeatureSwitchParams.QualityUprankingSigmoidBiasForHeavyRankingParam)
|
||||
)
|
||||
case _ => ModelBasedRanker.transformIdentity
|
||||
}
|
||||
|
||||
ModelBasedRanker.rankByQualityOoncCombinedScore(
|
||||
ooncRankedCandidates,
|
||||
transformFunc,
|
||||
target.params(PushFeatureSwitchParams.QualityUprankingBoostForHeavyRankingParam)
|
||||
)
|
||||
}
|
||||
} else ooncRankedCandidatesFut
|
||||
|
||||
if (target.params(
|
||||
PushFeatureSwitchParams.EnableProducersQualityBoostingForHeavyRankingParam)) {
|
||||
producerUprankingCounter.incr()
|
||||
qualityUprankedCandidatesFut.flatMap(cands =>
|
||||
ModelBasedRanker.rerankByProducerQualityOoncCombinedScore(cands)(statsReceiver))
|
||||
} else qualityUprankedCandidatesFut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def scoring(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
val ooncScoredCandidatesFut = target.rankingModelParam.map { rankingModelParam =>
|
||||
weightedOpenOrNtabClickModelScorer.scoreByBatchPredictionForModelVersion(
|
||||
target,
|
||||
candidates,
|
||||
rankingModelParam
|
||||
)
|
||||
}
|
||||
|
||||
val scoredCandidatesFut = {
|
||||
if (target.params(PushFeatureSwitchParams.EnableQualityUprankingForHeavyRankingParam)) {
|
||||
ooncScoredCandidatesFut.map { candidates =>
|
||||
weightedOpenOrNtabClickModelScorer.scoreByBatchPredictionForModelVersion(
|
||||
target = target,
|
||||
candidatesDetails = candidates,
|
||||
modelVersionParam = PushFeatureSwitchParams.QualityUprankingModelTypeParam,
|
||||
overridePushMLModelOpt = Some(PushMLModel.FilteringProbability)
|
||||
)
|
||||
}
|
||||
} else ooncScoredCandidatesFut
|
||||
}
|
||||
|
||||
scoredCandidatesFut.foreach { candidates =>
|
||||
val oonCandidates = candidates.filter {
|
||||
case CandidateDetails(pushCandidate: PushCandidate, _) =>
|
||||
ModelBasedRanker.tweetCandidateSelector(
|
||||
pushCandidate,
|
||||
MrQualityUprankingPartialTypeEnum.Oon)
|
||||
}
|
||||
setProducerQuality(
|
||||
target,
|
||||
oonCandidates,
|
||||
userHealthSignalStore,
|
||||
producerMediaRepresentationStore)
|
||||
}
|
||||
}
|
||||
|
||||
private def setProducerQuality(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]],
|
||||
userHealthSignalStore: ReadableStore[Long, UserHealthSignalResponse],
|
||||
producerMediaRepresentationStore: ReadableStore[Long, UserMediaRepresentation]
|
||||
): Unit = {
|
||||
lazy val boostRatio =
|
||||
target.params(PushFeatureSwitchParams.QualityUprankingBoostForHighQualityProducersParam)
|
||||
lazy val downboostRatio =
|
||||
target.params(PushFeatureSwitchParams.QualityUprankingDownboostForLowQualityProducersParam)
|
||||
candidates.foreach {
|
||||
case CandidateDetails(pushCandidate, _) =>
|
||||
HealthFeatureGetter
|
||||
.getFeatures(pushCandidate, producerMediaRepresentationStore, userHealthSignalStore).map {
|
||||
featureMap =>
|
||||
val agathaNsfwScore = featureMap.numericFeatures.getOrElse("agathaNsfwScore", 0.5)
|
||||
val textNsfwScore = featureMap.numericFeatures.getOrElse("textNsfwScore", 0.15)
|
||||
val nudityRate = featureMap.numericFeatures.getOrElse("nudityRate", 0.0)
|
||||
val activeFollowers = featureMap.numericFeatures.getOrElse("activeFollowers", 0.0)
|
||||
val favorsRcvd28Days = featureMap.numericFeatures.getOrElse("favorsRcvd28Days", 0.0)
|
||||
val tweets28Days = featureMap.numericFeatures.getOrElse("tweets28Days", 0.0)
|
||||
val authorDislikeCount = featureMap.numericFeatures
|
||||
.getOrElse("authorDislikeCount", 0.0)
|
||||
val authorDislikeRate = featureMap.numericFeatures.getOrElse("authorDislikeRate", 0.0)
|
||||
val authorReportRate = featureMap.numericFeatures.getOrElse("authorReportRate", 0.0)
|
||||
val abuseStrikeTop2Percent =
|
||||
featureMap.booleanFeatures.getOrElse("abuseStrikeTop2Percent", false)
|
||||
val abuseStrikeTop1Percent =
|
||||
featureMap.booleanFeatures.getOrElse("abuseStrikeTop1Percent", false)
|
||||
val hasNsfwToken = featureMap.booleanFeatures.getOrElse("hasNsfwToken", false)
|
||||
|
||||
if ((activeFollowers > 3000000) ||
|
||||
(activeFollowers > 1000000 && agathaNsfwScore < 0.7 && nudityRate < 0.01 && !hasNsfwToken && !abuseStrikeTop2Percent) ||
|
||||
(activeFollowers > 100000 && agathaNsfwScore < 0.7 && nudityRate < 0.01 && !hasNsfwToken && !abuseStrikeTop2Percent &&
|
||||
tweets28Days > 0 && favorsRcvd28Days / tweets28Days > 3000 && authorReportRate < 0.000001 && authorDislikeRate < 0.0005)) {
|
||||
producerBoostedCounter.incr()
|
||||
pushCandidate.setProducerQualityUprankingBoost(boostRatio)
|
||||
} else if (activeFollowers < 5 || agathaNsfwScore > 0.9 || nudityRate > 0.03 || hasNsfwToken || abuseStrikeTop1Percent ||
|
||||
textNsfwScore > 0.4 || (authorDislikeRate > 0.005 && authorDislikeCount > 5) ||
|
||||
(tweets28Days > 56 && favorsRcvd28Days / tweets28Days < 100)) {
|
||||
producerDownboostedCounter.incr()
|
||||
pushCandidate.setProducerQualityUprankingBoost(downboostRatio)
|
||||
} else pushCandidate.setProducerQualityUprankingBoost(1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def rerankBySubscriptionCreatorRanker(
|
||||
target: Target,
|
||||
rankedCandidates: Future[Seq[CandidateDetails[PushCandidate]]],
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
if (target.params(PushFeatureSwitchParams.SoftRankCandidatesFromSubscriptionCreators)) {
|
||||
val factor = target.params(PushFeatureSwitchParams.SoftRankFactorForSubscriptionCreators)
|
||||
subscriptionCreatorRanker.boostByScoreFactor(rankedCandidates, factor)
|
||||
} else
|
||||
subscriptionCreatorRanker.boostSubscriptionCreator(rankedCandidates)
|
||||
}
|
||||
|
||||
override def reRank(
|
||||
target: Target,
|
||||
rankedCandidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val numberOfF1Candidates =
|
||||
rankedCandidates.count(candidateDetails =>
|
||||
RecTypes.isF1Type(candidateDetails.candidate.commonRecType))
|
||||
lazy val threshold =
|
||||
target.params(PushFeatureSwitchParams.NumberOfF1CandidatesThresholdForOONBackfill)
|
||||
lazy val enableOONBackfillBasedOnF1 =
|
||||
target.params(PushFeatureSwitchParams.EnableOONBackfillBasedOnF1Candidates)
|
||||
|
||||
val f1BoostedCandidates =
|
||||
if (enableOONBackfillBasedOnF1 && numberOfF1Candidates > threshold) {
|
||||
boostCRTsRanker.boostCrtsToTopStableOrder(
|
||||
rankedCandidates,
|
||||
RecTypes.f1FirstDegreeTypes.toSeq)
|
||||
} else rankedCandidates
|
||||
|
||||
val topTweetsByGeoDownRankedCandidates =
|
||||
if (target.params(PushFeatureSwitchParams.BackfillRankTopTweetsByGeoCandidates)) {
|
||||
crtDownRanker.downRank(
|
||||
f1BoostedCandidates,
|
||||
Seq(CommonRecommendationType.GeoPopTweet)
|
||||
)
|
||||
} else f1BoostedCandidates
|
||||
|
||||
val reRankedCandidatesWithBoostedCrts = {
|
||||
val listOfCrtsToUpRank = target
|
||||
.params(PushFeatureSwitchParams.ListOfCrtsToUpRank)
|
||||
.flatMap(CommonRecommendationType.valueOf)
|
||||
crtsToUprank.add(listOfCrtsToUpRank.size)
|
||||
boostCRTsRanker.boostCrtsToTop(topTweetsByGeoDownRankedCandidates, listOfCrtsToUpRank)
|
||||
}
|
||||
|
||||
val reRankedCandidatesWithDownRankedCrts = {
|
||||
val listOfCrtsToDownRank = target
|
||||
.params(PushFeatureSwitchParams.ListOfCrtsToDownRank)
|
||||
.flatMap(CommonRecommendationType.valueOf)
|
||||
crtsToDownRank.add(listOfCrtsToDownRank.size)
|
||||
crtDownRanker.downRank(reRankedCandidatesWithBoostedCrts, listOfCrtsToDownRank)
|
||||
}
|
||||
|
||||
val rerankBySubscriptionCreatorFut = {
|
||||
if (target.params(PushFeatureSwitchParams.BoostCandidatesFromSubscriptionCreators)) {
|
||||
rerankBySubscriptionCreatorRanker(
|
||||
target,
|
||||
Future.value(reRankedCandidatesWithDownRankedCrts))
|
||||
} else Future.value(reRankedCandidatesWithDownRankedCrts)
|
||||
}
|
||||
|
||||
rerankBySubscriptionCreatorFut
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,110 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.rank
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.TweetAuthor
|
||||
import com.twitter.frigate.common.base.TweetCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.storehaus.FutureOps
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
class SubscriptionCreatorRanker(
|
||||
superFollowEligibilityUserStore: ReadableStore[Long, Boolean],
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private val scopedStats = statsReceiver.scope("SubscriptionCreatorRanker")
|
||||
private val boostStats = scopedStats.scope("boostSubscriptionCreator")
|
||||
private val softUprankStats = scopedStats.scope("boostByScoreFactor")
|
||||
private val boostTotalCandidates = boostStats.stat("total_input_candidates")
|
||||
private val softRankTotalCandidates = softUprankStats.stat("total_input_candidates")
|
||||
private val softRankNumCandidatesCreators = softUprankStats.counter("candidates_from_creators")
|
||||
private val softRankNumCandidatesNonCreators =
|
||||
softUprankStats.counter("candidates_not_from_creators")
|
||||
private val boostNumCandidatesCreators = boostStats.counter("candidates_from_creators")
|
||||
private val boostNumCandidatesNonCreators =
|
||||
boostStats.counter("candidates_not_from_creators")
|
||||
|
||||
def boostSubscriptionCreator(
|
||||
inputCandidatesFut: Future[Seq[CandidateDetails[PushCandidate]]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
inputCandidatesFut.flatMap { inputCandidates =>
|
||||
boostTotalCandidates.add(inputCandidates.size)
|
||||
val tweetAuthorIds = inputCandidates.flatMap {
|
||||
case CandidateDetails(candidate: TweetCandidate with TweetAuthor, s) =>
|
||||
candidate.authorId
|
||||
case _ => None
|
||||
}.toSet
|
||||
|
||||
FutureOps
|
||||
.mapCollect(superFollowEligibilityUserStore.multiGet(tweetAuthorIds))
|
||||
.map { creatorAuthorMap =>
|
||||
val (upRankedCandidates, otherCandidates) = inputCandidates.partition {
|
||||
case CandidateDetails(candidate: TweetCandidate with TweetAuthor, s) =>
|
||||
candidate.authorId match {
|
||||
case Some(authorId) =>
|
||||
creatorAuthorMap(authorId).getOrElse(false)
|
||||
case _ => false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
boostNumCandidatesCreators.incr(upRankedCandidates.size)
|
||||
boostNumCandidatesNonCreators.incr(otherCandidates.size)
|
||||
upRankedCandidates ++ otherCandidates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def boostByScoreFactor(
|
||||
inputCandidatesFut: Future[Seq[CandidateDetails[PushCandidate]]],
|
||||
factor: Double = 1.0,
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
inputCandidatesFut.flatMap { inputCandidates =>
|
||||
softRankTotalCandidates.add(inputCandidates.size)
|
||||
val tweetAuthorIds = inputCandidates.flatMap {
|
||||
case CandidateDetails(candidate: TweetCandidate with TweetAuthor, s) =>
|
||||
candidate.authorId
|
||||
case _ => None
|
||||
}.toSet
|
||||
|
||||
FutureOps
|
||||
.mapCollect(superFollowEligibilityUserStore.multiGet(tweetAuthorIds))
|
||||
.flatMap { creatorAuthorMap =>
|
||||
val (upRankedCandidates, otherCandidates) = inputCandidates.partition {
|
||||
case CandidateDetails(candidate: TweetCandidate with TweetAuthor, s) =>
|
||||
candidate.authorId match {
|
||||
case Some(authorId) =>
|
||||
creatorAuthorMap(authorId).getOrElse(false)
|
||||
case _ => false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
softRankNumCandidatesCreators.incr(upRankedCandidates.size)
|
||||
softRankNumCandidatesNonCreators.incr(otherCandidates.size)
|
||||
|
||||
ModelBasedRanker.rankBySpecifiedScore(
|
||||
inputCandidates,
|
||||
candidate => {
|
||||
val isFromCreator = candidate match {
|
||||
case candidate: TweetCandidate with TweetAuthor =>
|
||||
candidate.authorId match {
|
||||
case Some(authorId) =>
|
||||
creatorAuthorMap(authorId).getOrElse(false)
|
||||
case _ => false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
candidate.mrWeightedOpenOrNtabClickRankingProbability.map {
|
||||
case Some(score) =>
|
||||
if (isFromCreator) Some(score * factor)
|
||||
else Some(score)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,259 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.CandidateResult
|
||||
import com.twitter.frigate.common.base.CandidateSource
|
||||
import com.twitter.frigate.common.base.FetchRankFlowWithHydratedCandidates
|
||||
import com.twitter.frigate.common.base.Invalid
|
||||
import com.twitter.frigate.common.base.OK
|
||||
import com.twitter.frigate.common.base.Response
|
||||
import com.twitter.frigate.common.base.Result
|
||||
import com.twitter.frigate.common.base.Stats.track
|
||||
import com.twitter.frigate.common.base.Stats.trackSeq
|
||||
import com.twitter.frigate.common.logger.MRLogger
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.adaptor.LoggedOutPushCandidateSourceGenerator
|
||||
import com.twitter.frigate.pushservice.predicate.LoggedOutPreRankingPredicates
|
||||
import com.twitter.frigate.pushservice.predicate.LoggedOutTargetPredicates
|
||||
import com.twitter.frigate.pushservice.rank.LoggedOutRanker
|
||||
import com.twitter.frigate.pushservice.take.LoggedOutRefreshForPushNotifier
|
||||
import com.twitter.frigate.pushservice.scriber.MrRequestScribeHandler
|
||||
import com.twitter.frigate.pushservice.target.LoggedOutPushTargetUserBuilder
|
||||
import com.twitter.frigate.pushservice.thriftscala.LoggedOutRequest
|
||||
import com.twitter.frigate.pushservice.thriftscala.LoggedOutResponse
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushContext
|
||||
import com.twitter.hermit.predicate.NamedPredicate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
import com.twitter.hermit.predicate.SequentialPredicate
|
||||
import com.twitter.util.Future
|
||||
|
||||
class LoggedOutRefreshForPushHandler(
|
||||
val loPushTargetUserBuilder: LoggedOutPushTargetUserBuilder,
|
||||
val loPushCandidateSourceGenerator: LoggedOutPushCandidateSourceGenerator,
|
||||
candidateHydrator: PushCandidateHydrator,
|
||||
val loRanker: LoggedOutRanker,
|
||||
val loRfphNotifier: LoggedOutRefreshForPushNotifier,
|
||||
loMrRequestScriberNode: String
|
||||
)(
|
||||
globalStats: StatsReceiver)
|
||||
extends FetchRankFlowWithHydratedCandidates[Target, RawCandidate, PushCandidate] {
|
||||
|
||||
val log = MRLogger("LORefreshForPushHandler")
|
||||
implicit val statsReceiver: StatsReceiver =
|
||||
globalStats.scope("LORefreshForPushHandler")
|
||||
private val loggedOutBuildStats = statsReceiver.scope("logged_out_build_target")
|
||||
private val loggedOutProcessStats = statsReceiver.scope("logged_out_process")
|
||||
private val loggedOutNotifyStats = statsReceiver.scope("logged_out_notify")
|
||||
private val loCandidateHydrationStats: StatsReceiver =
|
||||
statsReceiver.scope("logged_out_candidate_hydration")
|
||||
val mrLORequestCandidateScribeStats =
|
||||
statsReceiver.scope("mr_logged_out_request_scribe_candidates")
|
||||
|
||||
val mrRequestScribeHandler =
|
||||
new MrRequestScribeHandler(loMrRequestScriberNode, statsReceiver.scope("lo_mr_request_scribe"))
|
||||
val loMrRequestTargetScribeStats = statsReceiver.scope("lo_mr_request_scribe_target")
|
||||
|
||||
lazy val loCandSourceEligibleCounter: Counter =
|
||||
loCandidateStats.counter("logged_out_cand_source_eligible")
|
||||
lazy val loCandSourceNotEligibleCounter: Counter =
|
||||
loCandidateStats.counter("logged_out_cand_source_not_eligible")
|
||||
lazy val allCandidatesCounter: Counter = statsReceiver.counter("all_logged_out_candidates")
|
||||
val allCandidatesFilteredPreRank = filterStats.counter("all_logged_out_candidates_filtered")
|
||||
|
||||
override def targetPredicates(target: Target): List[Predicate[Target]] = List(
|
||||
LoggedOutTargetPredicates.targetFatiguePredicate(),
|
||||
LoggedOutTargetPredicates.loggedOutRecsHoldbackPredicate()
|
||||
)
|
||||
|
||||
override def isTargetValid(target: Target): Future[Result] = {
|
||||
val resultFut =
|
||||
if (target.skipFilters) {
|
||||
Future.value(OK)
|
||||
} else {
|
||||
predicateSeq(target).track(Seq(target)).map { resultArr =>
|
||||
trackTargetPredStats(resultArr(0))
|
||||
}
|
||||
}
|
||||
track(targetStats)(resultFut)
|
||||
}
|
||||
|
||||
override def rank(
|
||||
target: Target,
|
||||
candidateDetails: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
loRanker.rank(candidateDetails)
|
||||
}
|
||||
|
||||
override def validCandidates(
|
||||
target: Target,
|
||||
candidates: Seq[PushCandidate]
|
||||
): Future[Seq[Result]] = {
|
||||
Future.value(candidates.map { c => OK })
|
||||
}
|
||||
|
||||
override def desiredCandidateCount(target: Target): Int = 1
|
||||
|
||||
private val loggedOutPreRankingPredicates =
|
||||
LoggedOutPreRankingPredicates(filterStats.scope("logged_out_predicates"))
|
||||
|
||||
private val loggedOutPreRankingPredicateChain =
|
||||
new SequentialPredicate[PushCandidate](loggedOutPreRankingPredicates)
|
||||
|
||||
override def filter(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[
|
||||
(Seq[CandidateDetails[PushCandidate]], Seq[CandidateResult[PushCandidate, Result]])
|
||||
] = {
|
||||
val predicateChain = loggedOutPreRankingPredicateChain
|
||||
predicateChain
|
||||
.track(candidates.map(_.candidate))
|
||||
.map { results =>
|
||||
val resultForPreRankingFiltering =
|
||||
results
|
||||
.zip(candidates)
|
||||
.foldLeft(
|
||||
(
|
||||
Seq.empty[CandidateDetails[PushCandidate]],
|
||||
Seq.empty[CandidateResult[PushCandidate, Result]]
|
||||
)
|
||||
) {
|
||||
case ((goodCandidates, filteredCandidates), (result, candidateDetails)) =>
|
||||
result match {
|
||||
case None =>
|
||||
(goodCandidates :+ candidateDetails, filteredCandidates)
|
||||
|
||||
case Some(pred: NamedPredicate[_]) =>
|
||||
val r = Invalid(Some(pred.name))
|
||||
(
|
||||
goodCandidates,
|
||||
filteredCandidates :+ CandidateResult[PushCandidate, Result](
|
||||
candidateDetails.candidate,
|
||||
candidateDetails.source,
|
||||
r
|
||||
)
|
||||
)
|
||||
case Some(_) =>
|
||||
val r = Invalid(Some("Filtered by un-named predicate"))
|
||||
(
|
||||
goodCandidates,
|
||||
filteredCandidates :+ CandidateResult[PushCandidate, Result](
|
||||
candidateDetails.candidate,
|
||||
candidateDetails.source,
|
||||
r
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
resultForPreRankingFiltering match {
|
||||
case (validCandidates, _) if validCandidates.isEmpty && candidates.nonEmpty =>
|
||||
allCandidatesFilteredPreRank.incr()
|
||||
case _ => ()
|
||||
|
||||
}
|
||||
resultForPreRankingFiltering
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override def candidateSources(
|
||||
target: Target
|
||||
): Future[Seq[CandidateSource[Target, RawCandidate]]] = {
|
||||
Future
|
||||
.collect(loPushCandidateSourceGenerator.sources.map { cs =>
|
||||
cs.isCandidateSourceAvailable(target).map { isEligible =>
|
||||
if (isEligible) {
|
||||
loCandSourceEligibleCounter.incr()
|
||||
Some(cs)
|
||||
} else {
|
||||
loCandSourceNotEligibleCounter.incr()
|
||||
None
|
||||
}
|
||||
}
|
||||
}).map(_.flatten)
|
||||
}
|
||||
|
||||
override def process(
|
||||
target: Target,
|
||||
externalCandidates: Seq[RawCandidate] = Nil
|
||||
): Future[Response[PushCandidate, Result]] = {
|
||||
isTargetValid(target).flatMap {
|
||||
case OK =>
|
||||
for {
|
||||
candidatesFromSources <- trackSeq(fetchStats)(fetchCandidates(target))
|
||||
externalCandidateDetails = externalCandidates.map(
|
||||
CandidateDetails(_, "logged_out_refresh_for_push_handler_external_candidates"))
|
||||
allCandidates = candidatesFromSources ++ externalCandidateDetails
|
||||
hydratedCandidatesWithCopy <-
|
||||
trackSeq(loCandidateHydrationStats)(hydrateCandidates(allCandidates))
|
||||
(candidates, preRankingFilteredCandidates) <-
|
||||
track(filterStats)(filter(target, hydratedCandidatesWithCopy))
|
||||
rankedCandidates <- trackSeq(rankingStats)(rank(target, candidates))
|
||||
allTakeCandidateResults <- track(takeStats)(
|
||||
take(target, rankedCandidates, desiredCandidateCount(target))
|
||||
)
|
||||
_ <- track(mrLORequestCandidateScribeStats)(
|
||||
mrRequestScribeHandler.scribeForCandidateFiltering(
|
||||
target,
|
||||
hydratedCandidatesWithCopy,
|
||||
preRankingFilteredCandidates,
|
||||
rankedCandidates,
|
||||
rankedCandidates,
|
||||
rankedCandidates,
|
||||
allTakeCandidateResults
|
||||
))
|
||||
|
||||
} yield {
|
||||
val takeCandidateResults = allTakeCandidateResults.filterNot { candResult =>
|
||||
candResult.result == MoreThanDesiredCandidates
|
||||
}
|
||||
val allCandidateResults = takeCandidateResults ++ preRankingFilteredCandidates
|
||||
allCandidatesCounter.incr(allCandidateResults.size)
|
||||
Response(OK, allCandidateResults)
|
||||
}
|
||||
|
||||
case result: Result =>
|
||||
for (_ <- track(loMrRequestTargetScribeStats)(
|
||||
mrRequestScribeHandler.scribeForTargetFiltering(target, result))) yield {
|
||||
Response(result, Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def buildTarget(
|
||||
guestId: Long,
|
||||
inputPushContext: Option[PushContext]
|
||||
): Future[Target] =
|
||||
loPushTargetUserBuilder.buildTarget(guestId, inputPushContext)
|
||||
|
||||
/**
|
||||
* Hydrate candidate by querying downstream services
|
||||
*
|
||||
* @param candidates - candidates
|
||||
*
|
||||
* @return - hydrated candidates
|
||||
*/
|
||||
override def hydrateCandidates(
|
||||
candidates: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = candidateHydrator(candidates)
|
||||
|
||||
override def batchForCandidatesCheck(target: Target): Int = 1
|
||||
|
||||
def refreshAndSend(request: LoggedOutRequest): Future[LoggedOutResponse] = {
|
||||
for {
|
||||
target <- track(loggedOutBuildStats)(
|
||||
loPushTargetUserBuilder.buildTarget(request.guestId, request.context))
|
||||
response <- track(loggedOutProcessStats)(process(target, externalCandidates = Seq.empty))
|
||||
loggedOutRefreshResponse <-
|
||||
track(loggedOutNotifyStats)(loRfphNotifier.checkResponseAndNotify(response))
|
||||
} yield {
|
||||
loggedOutRefreshResponse
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
@ -1,239 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.channels.common.thriftscala.ApiList
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.common.rec_types.RecTypes.isInNetworkTweetType
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.TrendTweetPushCandidate
|
||||
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||
import com.twitter.frigate.pushservice.refresh_handler.cross.CandidateCopyExpansion
|
||||
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil._
|
||||
import com.twitter.frigate.pushservice.util.MrUserStateUtil
|
||||
import com.twitter.frigate.pushservice.util.RelationshipUtil
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.hermit.predicate.socialgraph.RelationEdge
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class PushCandidateHydrator(
|
||||
socialGraphServiceProcessStore: ReadableStore[RelationEdge, Boolean],
|
||||
safeUserStore: ReadableStore[Long, User],
|
||||
apiListStore: ReadableStore[Long, ApiList],
|
||||
candidateCopyCross: CandidateCopyExpansion
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver,
|
||||
implicit val weightedOpenOrNtabClickModelScorer: PushMLModelScorer) {
|
||||
|
||||
lazy val candidateWithCopyNumStat = statsReceiver.stat("candidate_with_copy_num")
|
||||
lazy val hydratedCandidateStat = statsReceiver.scope("hydrated_candidates")
|
||||
lazy val mrUserStateStat = statsReceiver.scope("mr_user_state")
|
||||
|
||||
lazy val queryStep = statsReceiver.scope("query_step")
|
||||
lazy val relationEdgeWithoutDuplicateInQueryStep =
|
||||
queryStep.counter("number_of_relationEdge_without_duplicate_in_query_step")
|
||||
lazy val relationEdgeWithoutDuplicateInQueryStepDistribution =
|
||||
queryStep.stat("number_of_relationEdge_without_duplicate_in_query_step_distribution")
|
||||
|
||||
case class Entities(
|
||||
users: Set[Long] = Set.empty[Long],
|
||||
relationshipEdges: Set[RelationEdge] = Set.empty[RelationEdge]) {
|
||||
def merge(otherEntities: Entities): Entities = {
|
||||
this.copy(
|
||||
users = this.users ++ otherEntities.users,
|
||||
relationshipEdges =
|
||||
this.relationshipEdges ++ otherEntities.relationshipEdges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
case class EntitiesMap(
|
||||
userMap: Map[Long, User] = Map.empty[Long, User],
|
||||
relationshipMap: Map[RelationEdge, Boolean] = Map.empty[RelationEdge, Boolean])
|
||||
|
||||
private def updateCandidateAndCrtStats(
|
||||
candidate: RawCandidate,
|
||||
candidateType: String,
|
||||
numEntities: Int = 1
|
||||
): Unit = {
|
||||
statsReceiver
|
||||
.scope(candidateType).scope(candidate.commonRecType.name).stat(
|
||||
"totalEntitiesPerCandidateTypePerCrt").add(numEntities)
|
||||
statsReceiver.scope(candidateType).stat("totalEntitiesPerCandidateType").add(numEntities)
|
||||
}
|
||||
|
||||
private def collectEntities(
|
||||
candidateDetailsSeq: Seq[CandidateDetails[RawCandidate]]
|
||||
): Entities = {
|
||||
candidateDetailsSeq
|
||||
.map { candidateDetails =>
|
||||
val pushCandidate = candidateDetails.candidate
|
||||
|
||||
val userEntities = pushCandidate match {
|
||||
case tweetWithSocialContext: RawCandidate with TweetWithSocialContextTraits =>
|
||||
val authorIdOpt = getAuthorIdFromTweetCandidate(tweetWithSocialContext)
|
||||
val scUserIds = tweetWithSocialContext.socialContextUserIds.toSet
|
||||
updateCandidateAndCrtStats(pushCandidate, "tweetWithSocialContext", scUserIds.size + 1)
|
||||
Entities(users = scUserIds ++ authorIdOpt.toSet)
|
||||
|
||||
case _ => Entities()
|
||||
}
|
||||
|
||||
val relationEntities = {
|
||||
if (isInNetworkTweetType(pushCandidate.commonRecType)) {
|
||||
Entities(
|
||||
relationshipEdges =
|
||||
RelationshipUtil.getPreCandidateRelationshipsForInNetworkTweets(pushCandidate).toSet
|
||||
)
|
||||
} else Entities()
|
||||
}
|
||||
|
||||
userEntities.merge(relationEntities)
|
||||
}
|
||||
.foldLeft(Entities()) { (e1, e2) => e1.merge(e2) }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method calls Gizmoduck and Social Graph Service, keep the results in EntitiesMap
|
||||
* and passed onto the update candidate phase in the hydration step
|
||||
*
|
||||
* @param entities contains all userIds and relationEdges for all candidates
|
||||
* @return EntitiesMap contains userMap and relationshipMap
|
||||
*/
|
||||
private def queryEntities(entities: Entities): Future[EntitiesMap] = {
|
||||
|
||||
relationEdgeWithoutDuplicateInQueryStep.incr(entities.relationshipEdges.size)
|
||||
relationEdgeWithoutDuplicateInQueryStepDistribution.add(entities.relationshipEdges.size)
|
||||
|
||||
val relationshipMapFuture = Future
|
||||
.collect(socialGraphServiceProcessStore.multiGet(entities.relationshipEdges))
|
||||
.map { resultMap =>
|
||||
resultMap.collect {
|
||||
case (relationshipEdge, Some(res)) => relationshipEdge -> res
|
||||
case (relationshipEdge, None) => relationshipEdge -> false
|
||||
}
|
||||
}
|
||||
|
||||
val userMapFuture = Future
|
||||
.collect(safeUserStore.multiGet(entities.users))
|
||||
.map { userMap =>
|
||||
userMap.collect {
|
||||
case (userId, Some(user)) =>
|
||||
userId -> user
|
||||
}
|
||||
}
|
||||
|
||||
Future.join(userMapFuture, relationshipMapFuture).map {
|
||||
case (uMap, rMap) => EntitiesMap(userMap = uMap, relationshipMap = rMap)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param candidateDetails: recommendation candidates for a user
|
||||
* @return sequence of candidates tagged with push and ntab copy id
|
||||
*/
|
||||
private def expandCandidatesWithCopy(
|
||||
candidateDetails: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[(CandidateDetails[RawCandidate], CopyIds)]] = {
|
||||
candidateCopyCross.expandCandidatesWithCopyId(candidateDetails)
|
||||
}
|
||||
|
||||
def updateCandidates(
|
||||
candidateDetailsWithCopies: Seq[(CandidateDetails[RawCandidate], CopyIds)],
|
||||
entitiesMaps: EntitiesMap
|
||||
): Seq[CandidateDetails[PushCandidate]] = {
|
||||
candidateDetailsWithCopies.map {
|
||||
case (candidateDetail, copyIds) =>
|
||||
val pushCandidate = candidateDetail.candidate
|
||||
val userMap = entitiesMaps.userMap
|
||||
val relationshipMap = entitiesMaps.relationshipMap
|
||||
|
||||
val hydratedCandidate = pushCandidate match {
|
||||
|
||||
case f1TweetCandidate: F1FirstDegree =>
|
||||
getHydratedCandidateForF1FirstDegreeTweet(
|
||||
f1TweetCandidate,
|
||||
userMap,
|
||||
relationshipMap,
|
||||
copyIds)
|
||||
|
||||
case tweetRetweet: TweetRetweetCandidate =>
|
||||
getHydratedCandidateForTweetRetweet(tweetRetweet, userMap, copyIds)
|
||||
|
||||
case tweetFavorite: TweetFavoriteCandidate =>
|
||||
getHydratedCandidateForTweetFavorite(tweetFavorite, userMap, copyIds)
|
||||
|
||||
case tripTweetCandidate: OutOfNetworkTweetCandidate with TripCandidate =>
|
||||
getHydratedCandidateForTripTweetCandidate(tripTweetCandidate, userMap, copyIds)
|
||||
|
||||
case outOfNetworkTweetCandidate: OutOfNetworkTweetCandidate with TopicCandidate =>
|
||||
getHydratedCandidateForOutOfNetworkTweetCandidate(
|
||||
outOfNetworkTweetCandidate,
|
||||
userMap,
|
||||
copyIds)
|
||||
|
||||
case topicProofTweetCandidate: TopicProofTweetCandidate =>
|
||||
getHydratedTopicProofTweetCandidate(topicProofTweetCandidate, userMap, copyIds)
|
||||
|
||||
case subscribedSearchTweetCandidate: SubscribedSearchTweetCandidate =>
|
||||
getHydratedSubscribedSearchTweetCandidate(
|
||||
subscribedSearchTweetCandidate,
|
||||
userMap,
|
||||
copyIds)
|
||||
|
||||
case listRecommendation: ListPushCandidate =>
|
||||
getHydratedListCandidate(apiListStore, listRecommendation, copyIds)
|
||||
|
||||
case discoverTwitterCandidate: DiscoverTwitterCandidate =>
|
||||
getHydratedCandidateForDiscoverTwitterCandidate(discoverTwitterCandidate, copyIds)
|
||||
|
||||
case topTweetImpressionsCandidate: TopTweetImpressionsCandidate =>
|
||||
getHydratedCandidateForTopTweetImpressionsCandidate(
|
||||
topTweetImpressionsCandidate,
|
||||
copyIds)
|
||||
|
||||
case trendTweetCandidate: TrendTweetCandidate =>
|
||||
new TrendTweetPushCandidate(
|
||||
trendTweetCandidate,
|
||||
trendTweetCandidate.authorId.flatMap(userMap.get),
|
||||
copyIds)
|
||||
|
||||
case unknownCandidate =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Incorrect candidate for hydration: ${unknownCandidate.commonRecType}")
|
||||
}
|
||||
|
||||
CandidateDetails(
|
||||
hydratedCandidate,
|
||||
source = candidateDetail.source
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def apply(
|
||||
candidateDetails: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val isLoggedOutRequest =
|
||||
candidateDetails.headOption.exists(_.candidate.target.isLoggedOutUser)
|
||||
if (!isLoggedOutRequest) {
|
||||
candidateDetails.headOption.map { cd =>
|
||||
MrUserStateUtil.updateMrUserStateStats(cd.candidate.target)(mrUserStateStat)
|
||||
}
|
||||
}
|
||||
|
||||
expandCandidatesWithCopy(candidateDetails).flatMap { candidateDetailsWithCopy =>
|
||||
candidateWithCopyNumStat.add(candidateDetailsWithCopy.size)
|
||||
val entities = collectEntities(candidateDetailsWithCopy.map(_._1))
|
||||
queryEntities(entities).flatMap { entitiesMap =>
|
||||
val updatedCandidates = updateCandidates(candidateDetailsWithCopy, entitiesMap)
|
||||
updatedCandidates.foreach { cand =>
|
||||
hydratedCandidateStat.counter(cand.candidate.commonRecType.name).incr()
|
||||
}
|
||||
Future.value(updatedCandidates)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,69 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.FeatureMap
|
||||
import com.twitter.frigate.data_pipeline.features_common.MrRequestContextForFeatureStore
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.ml.HydrationContextBuilder
|
||||
import com.twitter.frigate.pushservice.params.PushParams
|
||||
import com.twitter.frigate.pushservice.util.MrUserStateUtil
|
||||
import com.twitter.nrel.heavyranker.FeatureHydrator
|
||||
import com.twitter.util.Future
|
||||
|
||||
class RFPHFeatureHydrator(
|
||||
featureHydrator: FeatureHydrator
|
||||
)(
|
||||
implicit globalStats: StatsReceiver) {
|
||||
|
||||
implicit val statsReceiver: StatsReceiver =
|
||||
globalStats.scope("RefreshForPushHandler")
|
||||
|
||||
//stat for feature hydration
|
||||
private val featureHydrationEnabledCounter = statsReceiver.counter("featureHydrationEnabled")
|
||||
private val mrUserStateStat = statsReceiver.scope("mr_user_state")
|
||||
|
||||
private def hydrateFromRelevanceHydrator(
|
||||
candidateDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
mrRequestContextForFeatureStore: MrRequestContextForFeatureStore
|
||||
): Future[Unit] = {
|
||||
val pushCandidates = candidateDetails.map(_.candidate)
|
||||
val candidatesAndContextsFut = Future.collect(pushCandidates.map { pc =>
|
||||
val contextFut = HydrationContextBuilder.build(pc)
|
||||
contextFut.map { ctx => (pc, ctx) }
|
||||
})
|
||||
candidatesAndContextsFut.flatMap { candidatesAndContexts =>
|
||||
val contexts = candidatesAndContexts.map(_._2)
|
||||
val resultsFut = featureHydrator.hydrateCandidate(contexts, mrRequestContextForFeatureStore)
|
||||
resultsFut.map { hydrationResult =>
|
||||
candidatesAndContexts.foreach {
|
||||
case (pushCandidate, context) =>
|
||||
val resultFeatures = hydrationResult.getOrElse(context, FeatureMap())
|
||||
pushCandidate.mergeFeatures(resultFeatures)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def candidateFeatureHydration(
|
||||
candidateDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
mrRequestContextForFeatureStore: MrRequestContextForFeatureStore
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
candidateDetails.headOption match {
|
||||
case Some(cand) =>
|
||||
val target = cand.candidate.target
|
||||
MrUserStateUtil.updateMrUserStateStats(target)(mrUserStateStat)
|
||||
if (target.params(PushParams.DisableAllRelevanceParam)) {
|
||||
Future.value(candidateDetails)
|
||||
} else {
|
||||
featureHydrationEnabledCounter.incr()
|
||||
for {
|
||||
_ <- hydrateFromRelevanceHydrator(candidateDetails, mrRequestContextForFeatureStore)
|
||||
} yield {
|
||||
candidateDetails
|
||||
}
|
||||
}
|
||||
case _ => Future.Nil
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,104 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.predicate.PreRankingPredicates
|
||||
import com.twitter.hermit.predicate.NamedPredicate
|
||||
import com.twitter.hermit.predicate.SequentialPredicate
|
||||
import com.twitter.util._
|
||||
|
||||
class RFPHPrerankFilter(
|
||||
)(
|
||||
globalStats: StatsReceiver) {
|
||||
def filter(
|
||||
target: Target,
|
||||
hydratedCandidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[
|
||||
(Seq[CandidateDetails[PushCandidate]], Seq[CandidateResult[PushCandidate, Result]])
|
||||
] = {
|
||||
lazy val filterStats: StatsReceiver = globalStats.scope("RefreshForPushHandler/filter")
|
||||
lazy val okFilterCounter: Counter = filterStats.counter("ok")
|
||||
lazy val invalidFilterCounter: Counter = filterStats.counter("invalid")
|
||||
lazy val invalidFilterStat: StatsReceiver = filterStats.scope("invalid")
|
||||
lazy val invalidFilterReasonStat: StatsReceiver = invalidFilterStat.scope("reason")
|
||||
val allCandidatesFilteredPreRank = filterStats.counter("all_candidates_filtered")
|
||||
|
||||
lazy val preRankingPredicates = PreRankingPredicates(
|
||||
filterStats.scope("predicates")
|
||||
)
|
||||
|
||||
lazy val preRankingPredicateChain =
|
||||
new SequentialPredicate[PushCandidate](preRankingPredicates)
|
||||
|
||||
val predicateChain = if (target.pushContext.exists(_.predicatesToEnable.exists(_.nonEmpty))) {
|
||||
val predicatesToEnable = target.pushContext.flatMap(_.predicatesToEnable).getOrElse(Nil)
|
||||
new SequentialPredicate[PushCandidate](preRankingPredicates.filter { pred =>
|
||||
predicatesToEnable.contains(pred.name)
|
||||
})
|
||||
} else preRankingPredicateChain
|
||||
|
||||
predicateChain
|
||||
.track(hydratedCandidates.map(_.candidate))
|
||||
.map { results =>
|
||||
val resultForPreRankFiltering = results
|
||||
.zip(hydratedCandidates)
|
||||
.foldLeft(
|
||||
(
|
||||
Seq.empty[CandidateDetails[PushCandidate]],
|
||||
Seq.empty[CandidateResult[PushCandidate, Result]]
|
||||
)
|
||||
) {
|
||||
case ((goodCandidates, filteredCandidates), (result, candidateDetails)) =>
|
||||
result match {
|
||||
case None =>
|
||||
okFilterCounter.incr()
|
||||
(goodCandidates :+ candidateDetails, filteredCandidates)
|
||||
|
||||
case Some(pred: NamedPredicate[_]) =>
|
||||
invalidFilterCounter.incr()
|
||||
invalidFilterReasonStat.counter(pred.name).incr()
|
||||
invalidFilterReasonStat
|
||||
.scope(candidateDetails.candidate.commonRecType.toString).counter(
|
||||
pred.name).incr()
|
||||
|
||||
val r = Invalid(Some(pred.name))
|
||||
(
|
||||
goodCandidates,
|
||||
filteredCandidates :+ CandidateResult[PushCandidate, Result](
|
||||
candidateDetails.candidate,
|
||||
candidateDetails.source,
|
||||
r
|
||||
)
|
||||
)
|
||||
case Some(_) =>
|
||||
invalidFilterCounter.incr()
|
||||
invalidFilterReasonStat.counter("unknown").incr()
|
||||
invalidFilterReasonStat
|
||||
.scope(candidateDetails.candidate.commonRecType.toString).counter(
|
||||
"unknown").incr()
|
||||
|
||||
val r = Invalid(Some("Filtered by un-named predicate"))
|
||||
(
|
||||
goodCandidates,
|
||||
filteredCandidates :+ CandidateResult[PushCandidate, Result](
|
||||
candidateDetails.candidate,
|
||||
candidateDetails.source,
|
||||
r
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
resultForPreRankFiltering match {
|
||||
case (validCandidates, _) if validCandidates.isEmpty && hydratedCandidates.nonEmpty =>
|
||||
allCandidatesFilteredPreRank.incr()
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
resultForPreRankFiltering
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,34 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.TargetUser
|
||||
import com.twitter.frigate.common.candidate.TargetABDecider
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.frigate.pushservice.target.TargetScoringDetails
|
||||
|
||||
class RFPHRestrictStep()(implicit stats: StatsReceiver) {
|
||||
|
||||
private val statsReceiver: StatsReceiver = stats.scope("RefreshForPushHandler")
|
||||
private val restrictStepStats: StatsReceiver = statsReceiver.scope("restrict")
|
||||
private val restrictStepNumCandidatesDroppedStat: Stat =
|
||||
restrictStepStats.stat("candidates_dropped")
|
||||
|
||||
/**
|
||||
* Limit the number of candidates that enter the Take step
|
||||
*/
|
||||
def restrict(
|
||||
target: TargetUser with TargetABDecider with TargetScoringDetails,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): (Seq[CandidateDetails[PushCandidate]], Seq[CandidateDetails[PushCandidate]]) = {
|
||||
if (target.params(PushFeatureSwitchParams.EnableRestrictStep)) {
|
||||
val restrictSizeParam = PushFeatureSwitchParams.RestrictStepSize
|
||||
val (newCandidates, filteredCandidates) = candidates.splitAt(target.params(restrictSizeParam))
|
||||
val numDropped = candidates.length - newCandidates.length
|
||||
restrictStepNumCandidatesDroppedStat.add(numDropped)
|
||||
(newCandidates, filteredCandidates)
|
||||
} else (candidates, Seq.empty)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,77 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
|
||||
class RFPHStatsRecorder(implicit statsReceiver: StatsReceiver) {
|
||||
|
||||
private val selectedCandidateScoreStats: StatsReceiver =
|
||||
statsReceiver.scope("score_of_sent_candidate_times_10000")
|
||||
|
||||
private val emptyScoreStats: StatsReceiver =
|
||||
statsReceiver.scope("score_of_sent_candidate_empty")
|
||||
|
||||
def trackPredictionScoreStats(candidate: PushCandidate): Unit = {
|
||||
candidate.mrWeightedOpenOrNtabClickRankingProbability.foreach {
|
||||
case Some(s) =>
|
||||
selectedCandidateScoreStats
|
||||
.stat("weighted_open_or_ntab_click_ranking")
|
||||
.add((s * 10000).toFloat)
|
||||
case None =>
|
||||
emptyScoreStats.counter("weighted_open_or_ntab_click_ranking").incr()
|
||||
}
|
||||
candidate.mrWeightedOpenOrNtabClickFilteringProbability.foreach {
|
||||
case Some(s) =>
|
||||
selectedCandidateScoreStats
|
||||
.stat("weighted_open_or_ntab_click_filtering")
|
||||
.add((s * 10000).toFloat)
|
||||
case None =>
|
||||
emptyScoreStats.counter("weighted_open_or_ntab_click_filtering").incr()
|
||||
}
|
||||
candidate.mrWeightedOpenOrNtabClickRankingProbability.foreach {
|
||||
case Some(s) =>
|
||||
selectedCandidateScoreStats
|
||||
.scope(candidate.commonRecType.toString)
|
||||
.stat("weighted_open_or_ntab_click_ranking")
|
||||
.add((s * 10000).toFloat)
|
||||
case None =>
|
||||
emptyScoreStats
|
||||
.scope(candidate.commonRecType.toString)
|
||||
.counter("weighted_open_or_ntab_click_ranking")
|
||||
.incr()
|
||||
}
|
||||
}
|
||||
|
||||
def refreshRequestExceptionStats(
|
||||
exception: Throwable,
|
||||
bStats: StatsReceiver
|
||||
): Unit = {
|
||||
bStats.counter("failures").incr()
|
||||
bStats.scope("failures").counter(exception.getClass.getCanonicalName).incr()
|
||||
}
|
||||
|
||||
def loggedOutRequestExceptionStats(
|
||||
exception: Throwable,
|
||||
bStats: StatsReceiver
|
||||
): Unit = {
|
||||
bStats.counter("logged_out_failures").incr()
|
||||
bStats.scope("failures").counter(exception.getClass.getCanonicalName).incr()
|
||||
}
|
||||
|
||||
def rankDistributionStats(
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
numRecsPerTypeStat: (CommonRecommendationType => Stat)
|
||||
): Unit = {
|
||||
candidatesDetails
|
||||
.groupBy { c =>
|
||||
c.candidate.commonRecType
|
||||
}
|
||||
.mapValues { s =>
|
||||
s.size
|
||||
}
|
||||
.foreach { case (crt, numRecs) => numRecsPerTypeStat(crt).add(numRecs) }
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,292 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.Stats.track
|
||||
import com.twitter.frigate.common.base.Stats.trackSeq
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.common.logger.MRLogger
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.adaptor._
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.frigate.pushservice.rank.RFPHLightRanker
|
||||
import com.twitter.frigate.pushservice.rank.RFPHRanker
|
||||
import com.twitter.frigate.pushservice.scriber.MrRequestScribeHandler
|
||||
import com.twitter.frigate.pushservice.take.candidate_validator.RFPHCandidateValidator
|
||||
import com.twitter.frigate.pushservice.target.PushTargetUserBuilder
|
||||
import com.twitter.frigate.pushservice.target.RFPHTargetPredicates
|
||||
import com.twitter.frigate.pushservice.util.RFPHTakeStepUtil
|
||||
import com.twitter.frigate.pushservice.util.AdhocStatsUtil
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushContext
|
||||
import com.twitter.frigate.pushservice.thriftscala.RefreshRequest
|
||||
import com.twitter.frigate.pushservice.thriftscala.RefreshResponse
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
import com.twitter.timelines.configapi.FeatureValue
|
||||
import com.twitter.util._
|
||||
|
||||
case class ResultWithDebugInfo(result: Result, predicateResults: Seq[PredicateWithResult])
|
||||
|
||||
class RefreshForPushHandler(
|
||||
val pushTargetUserBuilder: PushTargetUserBuilder,
|
||||
val candSourceGenerator: PushCandidateSourceGenerator,
|
||||
rfphRanker: RFPHRanker,
|
||||
candidateHydrator: PushCandidateHydrator,
|
||||
candidateValidator: RFPHCandidateValidator,
|
||||
rfphTakeStepUtil: RFPHTakeStepUtil,
|
||||
rfphRestrictStep: RFPHRestrictStep,
|
||||
val rfphNotifier: RefreshForPushNotifier,
|
||||
rfphStatsRecorder: RFPHStatsRecorder,
|
||||
mrRequestScriberNode: String,
|
||||
rfphFeatureHydrator: RFPHFeatureHydrator,
|
||||
rfphPrerankFilter: RFPHPrerankFilter,
|
||||
rfphLightRanker: RFPHLightRanker
|
||||
)(
|
||||
globalStats: StatsReceiver)
|
||||
extends FetchRankFlowWithHydratedCandidates[Target, RawCandidate, PushCandidate] {
|
||||
|
||||
val log = MRLogger("RefreshForPushHandler")
|
||||
|
||||
implicit val statsReceiver: StatsReceiver =
|
||||
globalStats.scope("RefreshForPushHandler")
|
||||
private val maxCandidatesToBatchInTakeStat: Stat =
|
||||
statsReceiver.stat("max_cands_to_batch_in_take")
|
||||
|
||||
private val rfphRequestCounter = statsReceiver.counter("requests")
|
||||
|
||||
private val buildTargetStats = statsReceiver.scope("build_target")
|
||||
private val processStats = statsReceiver.scope("process")
|
||||
private val notifyStats = statsReceiver.scope("notify")
|
||||
|
||||
private val lightRankingStats: StatsReceiver = statsReceiver.scope("light_ranking")
|
||||
private val reRankingStats: StatsReceiver = statsReceiver.scope("rerank")
|
||||
private val featureHydrationLatency: StatsReceiver =
|
||||
statsReceiver.scope("featureHydrationLatency")
|
||||
private val candidateHydrationStats: StatsReceiver = statsReceiver.scope("candidate_hydration")
|
||||
|
||||
lazy val candSourceEligibleCounter: Counter =
|
||||
candidateStats.counter("cand_source_eligible")
|
||||
lazy val candSourceNotEligibleCounter: Counter =
|
||||
candidateStats.counter("cand_source_not_eligible")
|
||||
|
||||
//pre-ranking stats
|
||||
val allCandidatesFilteredPreRank = filterStats.counter("all_candidates_filtered")
|
||||
|
||||
// total invalid candidates
|
||||
val totalStats: StatsReceiver = statsReceiver.scope("total")
|
||||
val totalInvalidCandidatesStat: Stat = totalStats.stat("candidates_invalid")
|
||||
|
||||
val mrRequestScribeBuiltStats: Counter = statsReceiver.counter("mr_request_scribe_built")
|
||||
|
||||
val mrRequestCandidateScribeStats = statsReceiver.scope("mr_request_scribe_candidates")
|
||||
val mrRequestTargetScribeStats = statsReceiver.scope("mr_request_scribe_target")
|
||||
|
||||
val mrRequestScribeHandler =
|
||||
new MrRequestScribeHandler(mrRequestScriberNode, statsReceiver.scope("mr_request_scribe"))
|
||||
|
||||
val adhocStatsUtil = new AdhocStatsUtil(statsReceiver.scope("adhoc_stats"))
|
||||
|
||||
private def numRecsPerTypeStat(crt: CommonRecommendationType) =
|
||||
fetchStats.scope(crt.toString).stat("dist")
|
||||
|
||||
// static list of target predicates
|
||||
private val targetPredicates = RFPHTargetPredicates(targetStats.scope("predicates"))
|
||||
|
||||
def buildTarget(
|
||||
userId: Long,
|
||||
inputPushContext: Option[PushContext],
|
||||
forcedFeatureValues: Option[Map[String, FeatureValue]] = None
|
||||
): Future[Target] =
|
||||
pushTargetUserBuilder.buildTarget(userId, inputPushContext, forcedFeatureValues)
|
||||
|
||||
override def targetPredicates(target: Target): List[Predicate[Target]] = targetPredicates
|
||||
|
||||
override def isTargetValid(target: Target): Future[Result] = {
|
||||
val resultFut = if (target.skipFilters) {
|
||||
Future.value(trackTargetPredStats(None))
|
||||
} else {
|
||||
predicateSeq(target).track(Seq(target)).map { resultArr =>
|
||||
trackTargetPredStats(resultArr(0))
|
||||
}
|
||||
}
|
||||
track(targetStats)(resultFut)
|
||||
}
|
||||
|
||||
override def candidateSources(
|
||||
target: Target
|
||||
): Future[Seq[CandidateSource[Target, RawCandidate]]] = {
|
||||
Future
|
||||
.collect(candSourceGenerator.sources.map { cs =>
|
||||
cs.isCandidateSourceAvailable(target).map { isEligible =>
|
||||
if (isEligible) {
|
||||
candSourceEligibleCounter.incr()
|
||||
Some(cs)
|
||||
} else {
|
||||
candSourceNotEligibleCounter.incr()
|
||||
None
|
||||
}
|
||||
}
|
||||
}).map(_.flatten)
|
||||
}
|
||||
|
||||
override def updateCandidateCounter(
|
||||
candidateResults: Seq[CandidateResult[PushCandidate, Result]]
|
||||
): Unit = {
|
||||
candidateResults.foreach {
|
||||
case candidateResult if candidateResult.result == OK =>
|
||||
okCandidateCounter.incr()
|
||||
case candidateResult if candidateResult.result.isInstanceOf[Invalid] =>
|
||||
invalidCandidateCounter.incr()
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
override def hydrateCandidates(
|
||||
candidates: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = candidateHydrator(candidates)
|
||||
|
||||
override def filter(
|
||||
target: Target,
|
||||
hydratedCandidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[
|
||||
(Seq[CandidateDetails[PushCandidate]], Seq[CandidateResult[PushCandidate, Result]])
|
||||
] = rfphPrerankFilter.filter(target, hydratedCandidates)
|
||||
|
||||
def lightRankAndTake(
|
||||
target: Target,
|
||||
candidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
rfphLightRanker.rank(target, candidates)
|
||||
}
|
||||
|
||||
override def rank(
|
||||
target: Target,
|
||||
candidatesDetails: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
val featureHydratedCandidatesFut = trackSeq(featureHydrationLatency)(
|
||||
rfphFeatureHydrator
|
||||
.candidateFeatureHydration(candidatesDetails, target.mrRequestContextForFeatureStore)
|
||||
)
|
||||
featureHydratedCandidatesFut.flatMap { featureHydratedCandidates =>
|
||||
rfphStatsRecorder.rankDistributionStats(featureHydratedCandidates, numRecsPerTypeStat)
|
||||
rfphRanker.initialRank(target, candidatesDetails)
|
||||
}
|
||||
}
|
||||
|
||||
def reRank(
|
||||
target: Target,
|
||||
rankedCandidates: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
rfphRanker.reRank(target, rankedCandidates)
|
||||
}
|
||||
|
||||
override def validCandidates(
|
||||
target: Target,
|
||||
candidates: Seq[PushCandidate]
|
||||
): Future[Seq[Result]] = {
|
||||
Future.collect(candidates.map { candidate =>
|
||||
rfphTakeStepUtil.isCandidateValid(candidate, candidateValidator).map(res => res.result)
|
||||
})
|
||||
}
|
||||
|
||||
override def desiredCandidateCount(target: Target): Int = target.desiredCandidateCount
|
||||
|
||||
override def batchForCandidatesCheck(target: Target): Int = {
|
||||
val fsParam = PushFeatureSwitchParams.NumberOfMaxCandidatesToBatchInRFPHTakeStep
|
||||
val maxToBatch = target.params(fsParam)
|
||||
maxCandidatesToBatchInTakeStat.add(maxToBatch)
|
||||
maxToBatch
|
||||
}
|
||||
|
||||
override def process(
|
||||
target: Target,
|
||||
externalCandidates: Seq[RawCandidate] = Nil
|
||||
): Future[Response[PushCandidate, Result]] = {
|
||||
isTargetValid(target).flatMap {
|
||||
case OK =>
|
||||
for {
|
||||
candidatesFromSources <- trackSeq(fetchStats)(fetchCandidates(target))
|
||||
externalCandidateDetails = externalCandidates.map(
|
||||
CandidateDetails(_, "refresh_for_push_handler_external_candidate"))
|
||||
allCandidates = candidatesFromSources ++ externalCandidateDetails
|
||||
hydratedCandidatesWithCopy <-
|
||||
trackSeq(candidateHydrationStats)(hydrateCandidates(allCandidates))
|
||||
_ = adhocStatsUtil.getCandidateSourceStats(hydratedCandidatesWithCopy)
|
||||
(candidates, preRankingFilteredCandidates) <-
|
||||
track(filterStats)(filter(target, hydratedCandidatesWithCopy))
|
||||
_ = adhocStatsUtil.getPreRankingFilterStats(preRankingFilteredCandidates)
|
||||
lightRankerFilteredCandidates <-
|
||||
trackSeq(lightRankingStats)(lightRankAndTake(target, candidates))
|
||||
_ = adhocStatsUtil.getLightRankingStats(lightRankerFilteredCandidates)
|
||||
rankedCandidates <- trackSeq(rankingStats)(rank(target, lightRankerFilteredCandidates))
|
||||
_ = adhocStatsUtil.getRankingStats(rankedCandidates)
|
||||
rerankedCandidates <- trackSeq(reRankingStats)(reRank(target, rankedCandidates))
|
||||
_ = adhocStatsUtil.getReRankingStats(rerankedCandidates)
|
||||
(restrictedCandidates, restrictFilteredCandidates) =
|
||||
rfphRestrictStep.restrict(target, rerankedCandidates)
|
||||
allTakeCandidateResults <- track(takeStats)(
|
||||
take(target, restrictedCandidates, desiredCandidateCount(target))
|
||||
)
|
||||
_ = adhocStatsUtil.getTakeCandidateResultStats(allTakeCandidateResults)
|
||||
_ <- track(mrRequestCandidateScribeStats)(
|
||||
mrRequestScribeHandler.scribeForCandidateFiltering(
|
||||
target,
|
||||
hydratedCandidatesWithCopy,
|
||||
preRankingFilteredCandidates,
|
||||
rankedCandidates,
|
||||
rerankedCandidates,
|
||||
restrictFilteredCandidates,
|
||||
allTakeCandidateResults
|
||||
))
|
||||
} yield {
|
||||
|
||||
/**
|
||||
* Take processes post restrict step candidates and returns both:
|
||||
* 1. valid + invalid candidates
|
||||
* 2. Candidates that are not processed (more than desired) + restricted candidates
|
||||
* We need #2 only for importance sampling
|
||||
*/
|
||||
val takeCandidateResults =
|
||||
allTakeCandidateResults.filterNot { candResult =>
|
||||
candResult.result == MoreThanDesiredCandidates
|
||||
}
|
||||
|
||||
val totalInvalidCandidates = {
|
||||
preRankingFilteredCandidates.size + //pre-ranking filtered candidates
|
||||
(rerankedCandidates.length - restrictedCandidates.length) + //candidates reject in restrict step
|
||||
takeCandidateResults.count(_.result != OK) //candidates reject in take step
|
||||
}
|
||||
takeInvalidCandidateDist.add(
|
||||
takeCandidateResults
|
||||
.count(_.result != OK)
|
||||
) // take step invalid candidates
|
||||
totalInvalidCandidatesStat.add(totalInvalidCandidates)
|
||||
val allCandidateResults = takeCandidateResults ++ preRankingFilteredCandidates
|
||||
Response(OK, allCandidateResults)
|
||||
}
|
||||
|
||||
case result: Result =>
|
||||
for (_ <- track(mrRequestTargetScribeStats)(
|
||||
mrRequestScribeHandler.scribeForTargetFiltering(target, result))) yield {
|
||||
mrRequestScribeBuiltStats.incr()
|
||||
Response(result, Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def refreshAndSend(request: RefreshRequest): Future[RefreshResponse] = {
|
||||
rfphRequestCounter.incr()
|
||||
for {
|
||||
target <- track(buildTargetStats)(
|
||||
pushTargetUserBuilder
|
||||
.buildTarget(request.userId, request.context))
|
||||
response <- track(processStats)(process(target, externalCandidates = Seq.empty))
|
||||
refreshResponse <- track(notifyStats)(rfphNotifier.checkResponseAndNotify(response, target))
|
||||
} yield {
|
||||
refreshResponse
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,128 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler
|
||||
|
||||
import com.twitter.finagle.stats.BroadcastStatsReceiver
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.Stats.track
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.common.config.CommonConstants
|
||||
import com.twitter.frigate.common.util.PushServiceUtil.FilteredRefreshResponseFut
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.take.CandidateNotifier
|
||||
import com.twitter.frigate.pushservice.util.ResponseStatsTrackUtils.trackStatsForResponseToRequest
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushStatus
|
||||
import com.twitter.frigate.pushservice.thriftscala.RefreshResponse
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.util.JavaTimer
|
||||
import com.twitter.util.Timer
|
||||
|
||||
class RefreshForPushNotifier(
|
||||
rfphStatsRecorder: RFPHStatsRecorder,
|
||||
candidateNotifier: CandidateNotifier
|
||||
)(
|
||||
globalStats: StatsReceiver) {
|
||||
|
||||
private implicit val statsReceiver: StatsReceiver =
|
||||
globalStats.scope("RefreshForPushHandler")
|
||||
|
||||
private val pushStats: StatsReceiver = statsReceiver.scope("push")
|
||||
private val sendLatency: StatsReceiver = statsReceiver.scope("send_handler")
|
||||
implicit private val timer: Timer = new JavaTimer(true)
|
||||
|
||||
private def notify(
|
||||
candidatesResult: CandidateResult[PushCandidate, Result],
|
||||
target: Target,
|
||||
receivers: Seq[StatsReceiver]
|
||||
): Future[RefreshResponse] = {
|
||||
|
||||
val candidate = candidatesResult.candidate
|
||||
|
||||
val predsResult = candidatesResult.result
|
||||
|
||||
if (predsResult != OK) {
|
||||
val invalidResult = predsResult
|
||||
invalidResult match {
|
||||
case Invalid(Some(reason)) =>
|
||||
Future.value(RefreshResponse(PushStatus.Filtered, Some(reason)))
|
||||
case _ =>
|
||||
Future.value(RefreshResponse(PushStatus.Filtered, None))
|
||||
}
|
||||
} else {
|
||||
rfphStatsRecorder.trackPredictionScoreStats(candidate)
|
||||
|
||||
val isQualityUprankingCandidate = candidate.mrQualityUprankingBoost.isDefined
|
||||
val commonRecTypeStats = Seq(
|
||||
statsReceiver.scope(candidate.commonRecType.toString),
|
||||
globalStats.scope(candidate.commonRecType.toString)
|
||||
)
|
||||
val qualityUprankingStats = Seq(
|
||||
statsReceiver.scope("QualityUprankingCandidates").scope(candidate.commonRecType.toString),
|
||||
globalStats.scope("QualityUprankingCandidates").scope(candidate.commonRecType.toString)
|
||||
)
|
||||
|
||||
val receiversWithRecTypeStats = {
|
||||
if (isQualityUprankingCandidate) {
|
||||
receivers ++ commonRecTypeStats ++ qualityUprankingStats
|
||||
} else {
|
||||
receivers ++ commonRecTypeStats
|
||||
}
|
||||
}
|
||||
track(sendLatency)(candidateNotifier.notify(candidate).map { res =>
|
||||
trackStatsForResponseToRequest(
|
||||
candidate.commonRecType,
|
||||
candidate.target,
|
||||
res,
|
||||
receiversWithRecTypeStats
|
||||
)(globalStats)
|
||||
RefreshResponse(res.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def checkResponseAndNotify(
|
||||
response: Response[PushCandidate, Result],
|
||||
targetUserContext: Target
|
||||
): Future[RefreshResponse] = {
|
||||
val receivers = Seq(statsReceiver)
|
||||
val refreshResponse = response match {
|
||||
case Response(OK, processedCandidates) =>
|
||||
// valid rec candidates
|
||||
val validCandidates = processedCandidates.filter(_.result == OK)
|
||||
|
||||
// top rec candidate
|
||||
validCandidates.headOption match {
|
||||
case Some(candidatesResult) =>
|
||||
candidatesResult.result match {
|
||||
case OK =>
|
||||
notify(candidatesResult, targetUserContext, receivers)
|
||||
.onSuccess { nr =>
|
||||
pushStats.scope("result").counter(nr.status.name).incr()
|
||||
}
|
||||
case _ =>
|
||||
targetUserContext.isTeamMember.flatMap { isTeamMember =>
|
||||
FilteredRefreshResponseFut
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
FilteredRefreshResponseFut
|
||||
}
|
||||
case Response(Invalid(reason), _) =>
|
||||
// invalid target with known reason
|
||||
FilteredRefreshResponseFut.map(_.copy(targetFilteredBy = reason))
|
||||
case _ =>
|
||||
// invalid target
|
||||
FilteredRefreshResponseFut
|
||||
}
|
||||
|
||||
val bStats = BroadcastStatsReceiver(receivers)
|
||||
Stat
|
||||
.timeFuture(bStats.stat("latency"))(
|
||||
refreshResponse
|
||||
.raiseWithin(CommonConstants.maxPushRequestDuration)
|
||||
)
|
||||
.onFailure { exception =>
|
||||
rfphStatsRecorder.refreshRequestExceptionStats(exception, bStats)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,79 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.util.MRNtabCopy
|
||||
import com.twitter.frigate.common.util.MRPushCopy
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.util.Future
|
||||
|
||||
abstract class BaseCopyFramework(statsReceiver: StatsReceiver) {
|
||||
|
||||
private val NoAvailableCopyStat = statsReceiver.scope("no_copy_for_crt")
|
||||
private val NoAvailableNtabCopyStat = statsReceiver.scope("no_ntab_copy")
|
||||
|
||||
/**
|
||||
* Instantiate push copy filters
|
||||
*/
|
||||
protected final val copyFilters = new CopyFilters(statsReceiver.scope("filters"))
|
||||
|
||||
/**
|
||||
*
|
||||
* The following method fetches all the push copies for a [[com.twitter.frigate.thriftscala.CommonRecommendationType]]
|
||||
* associated with a candidate and then filters the eligible copies based on
|
||||
* [[PushTypes.PushCandidate]] features. These filters are defined in
|
||||
* [[CopyFilters]]
|
||||
*
|
||||
* @param rawCandidate - [[RawCandidate]] object representing a recommendation candidate
|
||||
*
|
||||
* @return - set of eligible push copies for a given candidate
|
||||
*/
|
||||
protected[cross] final def getEligiblePushCopiesFromCandidate(
|
||||
rawCandidate: RawCandidate
|
||||
): Future[Seq[MRPushCopy]] = {
|
||||
val pushCopiesFromRectype = CandidateToCopy.getPushCopiesFromRectype(rawCandidate.commonRecType)
|
||||
|
||||
if (pushCopiesFromRectype.isEmpty) {
|
||||
NoAvailableCopyStat.counter(rawCandidate.commonRecType.name).incr()
|
||||
throw new IllegalStateException(s"No Copy defined for CRT: " + rawCandidate.commonRecType)
|
||||
}
|
||||
pushCopiesFromRectype
|
||||
.map(pushCopySet => copyFilters.execute(rawCandidate, pushCopySet.toSeq))
|
||||
.getOrElse(Future.value(Seq.empty))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This method essentially forms the base for cross-step for the MagicRecs Copy Framework. Given
|
||||
* a recommendation type this returns a set of tuples wherein each tuple is a pair of push and
|
||||
* ntab copy eligible for the said recommendation type
|
||||
*
|
||||
* @param rawCandidate - [[RawCandidate]] object representing a recommendation candidate
|
||||
* @return - Set of eligible [[MRPushCopy]], Option[[MRNtabCopy]] for a given recommendation type
|
||||
*/
|
||||
protected[cross] final def getEligiblePushAndNtabCopiesFromCandidate(
|
||||
rawCandidate: RawCandidate
|
||||
): Future[Seq[(MRPushCopy, Option[MRNtabCopy])]] = {
|
||||
|
||||
val eligiblePushCopies = getEligiblePushCopiesFromCandidate(rawCandidate)
|
||||
|
||||
eligiblePushCopies.map { pushCopies =>
|
||||
val setBuilder = Set.newBuilder[(MRPushCopy, Option[MRNtabCopy])]
|
||||
pushCopies.foreach { pushCopy =>
|
||||
val ntabCopies = CandidateToCopy.getNtabcopiesFromPushcopy(pushCopy)
|
||||
val pushNtabCopyPairs = ntabCopies match {
|
||||
case Some(ntabCopySet) =>
|
||||
if (ntabCopySet.isEmpty) {
|
||||
NoAvailableNtabCopyStat.counter(s"copy_id: ${pushCopy.copyId}").incr()
|
||||
Set(pushCopy -> None)
|
||||
} // push copy only
|
||||
else ntabCopySet.map(pushCopy -> Some(_))
|
||||
|
||||
case None =>
|
||||
Set.empty[(MRPushCopy, Option[MRNtabCopy])] // no push or ntab copy
|
||||
}
|
||||
setBuilder ++= pushNtabCopyPairs
|
||||
}
|
||||
setBuilder.result().toSeq
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,56 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.util.MRNtabCopy
|
||||
import com.twitter.frigate.common.util.MRPushCopy
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||
import com.twitter.util.Future
|
||||
|
||||
/**
|
||||
* @param statsReceiver - stats receiver object
|
||||
*/
|
||||
class CandidateCopyExpansion(statsReceiver: StatsReceiver)
|
||||
extends BaseCopyFramework(statsReceiver) {
|
||||
|
||||
/**
|
||||
*
|
||||
* Given a [[CandidateDetails]] object representing a push recommendation candidate this method
|
||||
* expands it to multiple candidates, each tagged with a push copy id and ntab copy id to
|
||||
* represent the eligible copies for the given recommendation candidate
|
||||
*
|
||||
* @param candidateDetails - [[CandidateDetails]] objects containing a recommendation candidate
|
||||
*
|
||||
* @return - list of tuples of [[PushTypes.RawCandidate]] and [[CopyIds]]
|
||||
*/
|
||||
private final def crossCandidateDetailsWithCopyId(
|
||||
candidateDetails: CandidateDetails[RawCandidate]
|
||||
): Future[Seq[(CandidateDetails[RawCandidate], CopyIds)]] = {
|
||||
val eligibleCopyPairs = getEligiblePushAndNtabCopiesFromCandidate(candidateDetails.candidate)
|
||||
val copyPairs = eligibleCopyPairs.map(_.map {
|
||||
case (pushCopy: MRPushCopy, ntabCopy: Option[MRNtabCopy]) =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(pushCopy.copyId),
|
||||
ntabCopyId = ntabCopy.map(_.copyId)
|
||||
)
|
||||
})
|
||||
|
||||
copyPairs.map(_.map((candidateDetails, _)))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This method takes as input a list of [[CandidateDetails]] objects which contain the push
|
||||
* recommendation candidates for a given target user. It expands each input candidate into
|
||||
* multiple candidates, each tagged with a push copy id and ntab copy id to represent the eligible
|
||||
* copies for the given recommendation candidate
|
||||
*
|
||||
* @param candidateDetailsSeq - list of fetched candidates for push recommendation
|
||||
* @return - list of tuples of [[RawCandidate]] and [[CopyIds]]
|
||||
*/
|
||||
final def expandCandidatesWithCopyId(
|
||||
candidateDetailsSeq: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[(CandidateDetails[RawCandidate], CopyIds)]] =
|
||||
Future.collect(candidateDetailsSeq.map(crossCandidateDetailsWithCopyId)).map(_.flatten)
|
||||
}
|
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.frigate.common.util.MRPushCopy
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
|
||||
/**
|
||||
*
|
||||
* @param candidate: [[RawCandidate]] is a recommendation candidate
|
||||
* @param pushCopy: [[MRPushCopy]] eligible for candidate
|
||||
*/
|
||||
case class CandidateCopyPair(candidate: RawCandidate, pushCopy: MRPushCopy)
|
Binary file not shown.
@ -1,263 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.frigate.common.util.MrNtabCopyObjects
|
||||
import com.twitter.frigate.common.util.MrPushCopyObjects
|
||||
import com.twitter.frigate.common.util._
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType._
|
||||
|
||||
object CandidateToCopy {
|
||||
|
||||
// Static map from a CommonRecommendationType to set of eligible push notification copies
|
||||
private[cross] val rectypeToPushCopy: Map[CommonRecommendationType, Set[
|
||||
MRPushCopy
|
||||
]] =
|
||||
Map[CommonRecommendationType, Set[MRPushCopy]](
|
||||
F1FirstdegreeTweet -> Set(
|
||||
MrPushCopyObjects.FirstDegreeJustTweetedBoldTitle
|
||||
),
|
||||
F1FirstdegreePhoto -> Set(
|
||||
MrPushCopyObjects.FirstDegreePhotoJustTweetedBoldTitle
|
||||
),
|
||||
F1FirstdegreeVideo -> Set(
|
||||
MrPushCopyObjects.FirstDegreeVideoJustTweetedBoldTitle
|
||||
),
|
||||
TweetRetweet -> Set(
|
||||
MrPushCopyObjects.TweetRetweetWithOneDisplaySocialContextsWithText,
|
||||
MrPushCopyObjects.TweetRetweetWithTwoDisplaySocialContextsWithText,
|
||||
MrPushCopyObjects.TweetRetweetWithOneDisplayAndKOtherSocialContextsWithText
|
||||
),
|
||||
TweetRetweetPhoto -> Set(
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithOneDisplaySocialContextWithText,
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithTwoDisplaySocialContextsWithText,
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithOneDisplayAndKOtherSocialContextsWithText
|
||||
),
|
||||
TweetRetweetVideo -> Set(
|
||||
MrPushCopyObjects.TweetRetweetVideoWithOneDisplaySocialContextWithText,
|
||||
MrPushCopyObjects.TweetRetweetVideoWithTwoDisplaySocialContextsWithText,
|
||||
MrPushCopyObjects.TweetRetweetVideoWithOneDisplayAndKOtherSocialContextsWithText
|
||||
),
|
||||
TweetFavorite -> Set(
|
||||
MrPushCopyObjects.TweetLikeOneSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikeTwoSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikeMultipleSocialContextWithText
|
||||
),
|
||||
TweetFavoritePhoto -> Set(
|
||||
MrPushCopyObjects.TweetLikePhotoOneSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikePhotoTwoSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikePhotoMultipleSocialContextWithText
|
||||
),
|
||||
TweetFavoriteVideo -> Set(
|
||||
MrPushCopyObjects.TweetLikeVideoOneSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikeVideoTwoSocialContextWithText,
|
||||
MrPushCopyObjects.TweetLikeVideoMultipleSocialContextWithText
|
||||
),
|
||||
UnreadBadgeCount -> Set(MrPushCopyObjects.UnreadBadgeCount),
|
||||
InterestBasedTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
InterestBasedPhoto -> Set(MrPushCopyObjects.RecommendedForYouPhoto),
|
||||
InterestBasedVideo -> Set(MrPushCopyObjects.RecommendedForYouVideo),
|
||||
UserFollow -> Set(
|
||||
MrPushCopyObjects.UserFollowWithOneSocialContext,
|
||||
MrPushCopyObjects.UserFollowWithTwoSocialContext,
|
||||
MrPushCopyObjects.UserFollowOneDisplayAndKOtherSocialContext
|
||||
),
|
||||
HermitUser -> Set(
|
||||
MrPushCopyObjects.HermitUserWithOneSocialContext,
|
||||
MrPushCopyObjects.HermitUserWithTwoSocialContext,
|
||||
MrPushCopyObjects.HermitUserWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
TriangularLoopUser -> Set(
|
||||
MrPushCopyObjects.TriangularLoopUserWithOneSocialContext,
|
||||
MrPushCopyObjects.TriangularLoopUserWithTwoSocialContexts,
|
||||
MrPushCopyObjects.TriangularLoopUserOneDisplayAndKotherSocialContext
|
||||
),
|
||||
ForwardAddressbookUserFollow -> Set(MrPushCopyObjects.ForwardAddressBookUserFollow),
|
||||
NewsArticleNewsLanding -> Set(MrPushCopyObjects.NewsArticleNewsLandingCopy),
|
||||
TopicProofTweet -> Set(MrPushCopyObjects.TopicProofTweet),
|
||||
UserInterestinTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
UserInterestinPhoto -> Set(MrPushCopyObjects.RecommendedForYouPhoto),
|
||||
UserInterestinVideo -> Set(MrPushCopyObjects.RecommendedForYouVideo),
|
||||
TwistlyTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
TwistlyPhoto -> Set(MrPushCopyObjects.RecommendedForYouPhoto),
|
||||
TwistlyVideo -> Set(MrPushCopyObjects.RecommendedForYouVideo),
|
||||
ElasticTimelineTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
ElasticTimelinePhoto -> Set(MrPushCopyObjects.RecommendedForYouPhoto),
|
||||
ElasticTimelineVideo -> Set(MrPushCopyObjects.RecommendedForYouVideo),
|
||||
ExploreVideoTweet -> Set(MrPushCopyObjects.ExploreVideoTweet),
|
||||
List -> Set(MrPushCopyObjects.ListRecommendation),
|
||||
InterestBasedUserFollow -> Set(MrPushCopyObjects.UserFollowInterestBasedCopy),
|
||||
PastEmailEngagementTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
PastEmailEngagementPhoto -> Set(MrPushCopyObjects.RecommendedForYouPhoto),
|
||||
PastEmailEngagementVideo -> Set(MrPushCopyObjects.RecommendedForYouVideo),
|
||||
ExplorePush -> Set(MrPushCopyObjects.ExplorePush),
|
||||
ConnectTabPush -> Set(MrPushCopyObjects.ConnectTabPush),
|
||||
ConnectTabWithUserPush -> Set(MrPushCopyObjects.ConnectTabWithUserPush),
|
||||
AddressBookUploadPush -> Set(MrPushCopyObjects.AddressBookPush),
|
||||
InterestPickerPush -> Set(MrPushCopyObjects.InterestPickerPush),
|
||||
CompleteOnboardingPush -> Set(MrPushCopyObjects.CompleteOnboardingPush),
|
||||
GeoPopTweet -> Set(MrPushCopyObjects.GeoPopPushCopy),
|
||||
TagSpaceTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
FrsTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
TwhinTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
MrModelingBasedTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
DetopicTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
TweetImpressions -> Set(MrPushCopyObjects.TopTweetImpressions),
|
||||
TrendTweet -> Set(MrPushCopyObjects.TrendTweet),
|
||||
ReverseAddressbookTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
ForwardAddressbookTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
SpaceInNetwork -> Set(MrPushCopyObjects.SpaceHost),
|
||||
SpaceOutOfNetwork -> Set(MrPushCopyObjects.SpaceHost),
|
||||
SubscribedSearch -> Set(MrPushCopyObjects.SubscribedSearchTweet),
|
||||
TripGeoTweet -> Set(MrPushCopyObjects.TripGeoTweetPushCopy),
|
||||
CrowdSearchTweet -> Set(MrPushCopyObjects.RecommendedForYouTweet),
|
||||
Digest -> Set(MrPushCopyObjects.Digest),
|
||||
TripHqTweet -> Set(MrPushCopyObjects.TripHqTweetPushCopy)
|
||||
)
|
||||
|
||||
// Static map from a push copy to set of eligible ntab copies
|
||||
private[cross] val pushcopyToNtabcopy: Map[MRPushCopy, Set[MRNtabCopy]] =
|
||||
Map[MRPushCopy, Set[MRNtabCopy]](
|
||||
MrPushCopyObjects.FirstDegreeJustTweetedBoldTitle -> Set(
|
||||
MrNtabCopyObjects.FirstDegreeTweetRecent),
|
||||
MrPushCopyObjects.FirstDegreePhotoJustTweetedBoldTitle -> Set(
|
||||
MrNtabCopyObjects.FirstDegreeTweetRecent
|
||||
),
|
||||
MrPushCopyObjects.FirstDegreeVideoJustTweetedBoldTitle -> Set(
|
||||
MrNtabCopyObjects.FirstDegreeTweetRecent
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetWithOneDisplaySocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetWithTwoDisplaySocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetWithOneDisplayAndKOtherSocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithOneDisplaySocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetPhotoWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithTwoDisplaySocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetPhotoWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetPhotoWithOneDisplayAndKOtherSocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetPhotoWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetVideoWithOneDisplaySocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetVideoWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetVideoWithTwoDisplaySocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetVideoWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetRetweetVideoWithOneDisplayAndKOtherSocialContextsWithText -> Set(
|
||||
MrNtabCopyObjects.TweetRetweetVideoWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeOneSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeTwoSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeMultipleSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikePhotoOneSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikePhotoWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetLikePhotoTwoSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikePhotoWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikePhotoMultipleSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikePhotoWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeVideoOneSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeVideoWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeVideoTwoSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeVideoWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TweetLikeVideoMultipleSocialContextWithText -> Set(
|
||||
MrNtabCopyObjects.TweetLikeVideoWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.UnreadBadgeCount -> Set.empty[MRNtabCopy],
|
||||
MrPushCopyObjects.RecommendedForYouTweet -> Set(MrNtabCopyObjects.RecommendedForYouCopy),
|
||||
MrPushCopyObjects.RecommendedForYouPhoto -> Set(MrNtabCopyObjects.RecommendedForYouCopy),
|
||||
MrPushCopyObjects.RecommendedForYouVideo -> Set(MrNtabCopyObjects.RecommendedForYouCopy),
|
||||
MrPushCopyObjects.GeoPopPushCopy -> Set(MrNtabCopyObjects.RecommendedForYouCopy),
|
||||
MrPushCopyObjects.UserFollowWithOneSocialContext -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.UserFollowWithTwoSocialContext -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.UserFollowOneDisplayAndKOtherSocialContext -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.HermitUserWithOneSocialContext -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithOneDisplaySocialContext
|
||||
),
|
||||
MrPushCopyObjects.HermitUserWithTwoSocialContext -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithTwoDisplaySocialContexts
|
||||
),
|
||||
MrPushCopyObjects.HermitUserWithOneDisplayAndKOtherSocialContexts -> Set(
|
||||
MrNtabCopyObjects.UserFollowWithOneDisplayAndKOtherSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TriangularLoopUserWithOneSocialContext -> Set(
|
||||
MrNtabCopyObjects.TriangularLoopUserWithOneSocialContext
|
||||
),
|
||||
MrPushCopyObjects.TriangularLoopUserWithTwoSocialContexts -> Set(
|
||||
MrNtabCopyObjects.TriangularLoopUserWithTwoSocialContexts
|
||||
),
|
||||
MrPushCopyObjects.TriangularLoopUserOneDisplayAndKotherSocialContext -> Set(
|
||||
MrNtabCopyObjects.TriangularLoopUserOneDisplayAndKOtherSocialContext
|
||||
),
|
||||
MrPushCopyObjects.NewsArticleNewsLandingCopy -> Set(
|
||||
MrNtabCopyObjects.NewsArticleNewsLandingCopy
|
||||
),
|
||||
MrPushCopyObjects.UserFollowInterestBasedCopy -> Set(
|
||||
MrNtabCopyObjects.UserFollowInterestBasedCopy
|
||||
),
|
||||
MrPushCopyObjects.ForwardAddressBookUserFollow -> Set(
|
||||
MrNtabCopyObjects.ForwardAddressBookUserFollow),
|
||||
MrPushCopyObjects.ConnectTabPush -> Set(
|
||||
MrNtabCopyObjects.ConnectTabPush
|
||||
),
|
||||
MrPushCopyObjects.ExplorePush -> Set.empty[MRNtabCopy],
|
||||
MrPushCopyObjects.ConnectTabWithUserPush -> Set(
|
||||
MrNtabCopyObjects.UserFollowInterestBasedCopy),
|
||||
MrPushCopyObjects.AddressBookPush -> Set(MrNtabCopyObjects.AddressBook),
|
||||
MrPushCopyObjects.InterestPickerPush -> Set(MrNtabCopyObjects.InterestPicker),
|
||||
MrPushCopyObjects.CompleteOnboardingPush -> Set(MrNtabCopyObjects.CompleteOnboarding),
|
||||
MrPushCopyObjects.TopicProofTweet -> Set(MrNtabCopyObjects.TopicProofTweet),
|
||||
MrPushCopyObjects.TopTweetImpressions -> Set(MrNtabCopyObjects.TopTweetImpressions),
|
||||
MrPushCopyObjects.TrendTweet -> Set(MrNtabCopyObjects.TrendTweet),
|
||||
MrPushCopyObjects.SpaceHost -> Set(MrNtabCopyObjects.SpaceHost),
|
||||
MrPushCopyObjects.SubscribedSearchTweet -> Set(MrNtabCopyObjects.SubscribedSearchTweet),
|
||||
MrPushCopyObjects.TripGeoTweetPushCopy -> Set(MrNtabCopyObjects.RecommendedForYouCopy),
|
||||
MrPushCopyObjects.Digest -> Set(MrNtabCopyObjects.Digest),
|
||||
MrPushCopyObjects.TripHqTweetPushCopy -> Set(MrNtabCopyObjects.HighQualityTweet),
|
||||
MrPushCopyObjects.ExploreVideoTweet -> Set(MrNtabCopyObjects.ExploreVideoTweet),
|
||||
MrPushCopyObjects.ListRecommendation -> Set(MrNtabCopyObjects.ListRecommendation),
|
||||
MrPushCopyObjects.MagicFanoutCreatorSubscription -> Set(
|
||||
MrNtabCopyObjects.MagicFanoutCreatorSubscription),
|
||||
MrPushCopyObjects.MagicFanoutNewCreator -> Set(MrNtabCopyObjects.MagicFanoutNewCreator)
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param crt - [[CommonRecommendationType]] used for a frigate push notification
|
||||
*
|
||||
* @return - Set of [[MRPushCopy]] objects representing push copies eligibile for a
|
||||
* [[CommonRecommendationType]]
|
||||
*/
|
||||
def getPushCopiesFromRectype(crt: CommonRecommendationType): Option[Set[MRPushCopy]] =
|
||||
rectypeToPushCopy.get(crt)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pushcopy - [[MRPushCopy]] object representing a push notification copy
|
||||
* @return - Set of [[MRNtabCopy]] objects that can be paired with a given [[MRPushCopy]]
|
||||
*/
|
||||
def getNtabcopiesFromPushcopy(pushcopy: MRPushCopy): Option[Set[MRNtabCopy]] =
|
||||
pushcopyToNtabcopy.get(pushcopy)
|
||||
}
|
Binary file not shown.
@ -1,41 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.common.util.MRPushCopy
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
import com.twitter.util.Future
|
||||
|
||||
private[cross] class CopyFilters(statsReceiver: StatsReceiver) {
|
||||
|
||||
private val copyPredicates = new CopyPredicates(statsReceiver.scope("copy_predicate"))
|
||||
|
||||
def execute(rawCandidate: RawCandidate, pushCopies: Seq[MRPushCopy]): Future[Seq[MRPushCopy]] = {
|
||||
val candidateCopyPairs: Seq[CandidateCopyPair] =
|
||||
pushCopies.map(CandidateCopyPair(rawCandidate, _))
|
||||
|
||||
val compositePredicate: Predicate[CandidateCopyPair] = rawCandidate match {
|
||||
case _: F1FirstDegree | _: OutOfNetworkTweetCandidate | _: EventCandidate |
|
||||
_: TopicProofTweetCandidate | _: ListPushCandidate | _: HermitInterestBasedUserFollow |
|
||||
_: UserFollowWithoutSocialContextCandidate | _: DiscoverTwitterCandidate |
|
||||
_: TopTweetImpressionsCandidate | _: TrendTweetCandidate |
|
||||
_: SubscribedSearchTweetCandidate | _: DigestCandidate =>
|
||||
copyPredicates.alwaysTruePredicate
|
||||
|
||||
case _: SocialContextActions => copyPredicates.displaySocialContextPredicate
|
||||
|
||||
case _ => copyPredicates.unrecognizedCandidatePredicate // block unrecognised candidates
|
||||
}
|
||||
|
||||
// apply predicate to all [[MRPushCopy]] objects
|
||||
val filterResults: Future[Seq[Boolean]] = compositePredicate(candidateCopyPairs)
|
||||
filterResults.map { results: Seq[Boolean] =>
|
||||
val seqBuilder = Seq.newBuilder[MRPushCopy]
|
||||
results.zip(pushCopies).foreach {
|
||||
case (result, pushCopy) => if (result) seqBuilder += pushCopy
|
||||
}
|
||||
seqBuilder.result()
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,36 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.refresh_handler.cross
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.SocialContextActions
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.hermit.predicate.Predicate
|
||||
|
||||
class CopyPredicates(statsReceiver: StatsReceiver) {
|
||||
val alwaysTruePredicate = Predicate
|
||||
.from { _: CandidateCopyPair =>
|
||||
true
|
||||
}.withStats(statsReceiver.scope("always_true_copy_predicate"))
|
||||
|
||||
val unrecognizedCandidatePredicate = alwaysTruePredicate.flip
|
||||
.withStats(statsReceiver.scope("unrecognized_candidate"))
|
||||
|
||||
val displaySocialContextPredicate = Predicate
|
||||
.from { candidateCopyPair: CandidateCopyPair =>
|
||||
candidateCopyPair.candidate match {
|
||||
case candidateWithScActions: RawCandidate with SocialContextActions =>
|
||||
val socialContextUserIds = candidateWithScActions.socialContextActions.map(_.userId)
|
||||
val countSocialContext = socialContextUserIds.size
|
||||
val pushCopy = candidateCopyPair.pushCopy
|
||||
|
||||
countSocialContext match {
|
||||
case 1 => pushCopy.hasOneDisplaySocialContext && !pushCopy.hasOtherSocialContext
|
||||
case 2 => pushCopy.hasTwoDisplayContext && !pushCopy.hasOtherSocialContext
|
||||
case c if c > 2 =>
|
||||
pushCopy.hasOneDisplaySocialContext && pushCopy.hasOtherSocialContext
|
||||
case _ => false
|
||||
}
|
||||
|
||||
case _ => false
|
||||
}
|
||||
}.withStats(statsReceiver.scope("display_social_context_predicate"))
|
||||
}
|
Binary file not shown.
@ -1,388 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.scriber
|
||||
|
||||
import com.twitter.bijection.Base64String
|
||||
import com.twitter.bijection.Injection
|
||||
import com.twitter.bijection.scrooge.BinaryScalaCodec
|
||||
import com.twitter.core_workflows.user_model.thriftscala.{UserState => ThriftUserState}
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finagle.tracing.Trace
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.CandidateResult
|
||||
import com.twitter.frigate.common.base.Invalid
|
||||
import com.twitter.frigate.common.base.OK
|
||||
import com.twitter.frigate.common.base.Result
|
||||
import com.twitter.frigate.common.rec_types.RecTypes
|
||||
import com.twitter.frigate.data_pipeline.features_common.PushQualityModelFeatureContext
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||
import com.twitter.frigate.pushservice.params.PushParams
|
||||
import com.twitter.frigate.scribe.thriftscala.CandidateFilteredOutStep
|
||||
import com.twitter.frigate.scribe.thriftscala.CandidateRequestInfo
|
||||
import com.twitter.frigate.scribe.thriftscala.MrRequestScribe
|
||||
import com.twitter.frigate.scribe.thriftscala.TargetUserInfo
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.frigate.thriftscala.TweetNotification
|
||||
import com.twitter.frigate.thriftscala.{SocialContextAction => TSocialContextAction}
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.ml.api.DataRecord
|
||||
import com.twitter.ml.api.Feature
|
||||
import com.twitter.ml.api.FeatureType
|
||||
import com.twitter.ml.api.util.SRichDataRecord
|
||||
import com.twitter.ml.api.util.ScalaToJavaDataRecordConversions
|
||||
import com.twitter.nrel.heavyranker.PushPredictionHelper
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.util.Time
|
||||
import java.util.UUID
|
||||
import scala.collection.mutable
|
||||
|
||||
class MrRequestScribeHandler(mrRequestScriberNode: String, stats: StatsReceiver) {
|
||||
|
||||
private val mrRequestScribeLogger = Logger(mrRequestScriberNode)
|
||||
|
||||
private val mrRequestScribeTargetFilteringStats =
|
||||
stats.counter("MrRequestScribeHandler_target_filtering")
|
||||
private val mrRequestScribeCandidateFilteringStats =
|
||||
stats.counter("MrRequestScribeHandler_candidate_filtering")
|
||||
private val mrRequestScribeInvalidStats =
|
||||
stats.counter("MrRequestScribeHandler_invalid_filtering")
|
||||
private val mrRequestScribeUnsupportedFeatureTypeStats =
|
||||
stats.counter("MrRequestScribeHandler_unsupported_feature_type")
|
||||
private val mrRequestScribeNotIncludedFeatureStats =
|
||||
stats.counter("MrRequestScribeHandler_not_included_features")
|
||||
|
||||
private final val MrRequestScribeInjection: Injection[MrRequestScribe, String] = BinaryScalaCodec(
|
||||
MrRequestScribe
|
||||
) andThen Injection.connect[Array[Byte], Base64String, String]
|
||||
|
||||
/**
|
||||
*
|
||||
* @param target : Target user id
|
||||
* @param result : Result for target filtering
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def scribeForTargetFiltering(target: Target, result: Result): Future[Option[MrRequestScribe]] = {
|
||||
if (target.isLoggedOutUser || !enableTargetFilteringScribing(target)) {
|
||||
Future.None
|
||||
} else {
|
||||
val predicate = result match {
|
||||
case Invalid(reason) => reason
|
||||
case _ =>
|
||||
mrRequestScribeInvalidStats.incr()
|
||||
throw new IllegalStateException("Invalid reason for Target Filtering " + result)
|
||||
}
|
||||
buildScribeThrift(target, predicate, None).map { targetFilteredScribe =>
|
||||
writeAtTargetFilteringStep(target, targetFilteredScribe)
|
||||
Some(targetFilteredScribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param target : Target user id
|
||||
* @param hydratedCandidates : Candidates hydrated with details: impressionId, frigateNotification and source
|
||||
* @param preRankingFilteredCandidates : Candidates result filtered out at preRanking filtering step
|
||||
* @param rankedCandidates : Sorted candidates details ranked by ranking step
|
||||
* @param rerankedCandidates : Sorted candidates details ranked by reranking step
|
||||
* @param restrictFilteredCandidates : Candidates details filtered out at restrict step
|
||||
* @param allTakeCandidateResults : Candidates results at take step, include the candidates we take and the candidates filtered out at take step [with different result]
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def scribeForCandidateFiltering(
|
||||
target: Target,
|
||||
hydratedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
preRankingFilteredCandidates: Seq[CandidateResult[PushCandidate, Result]],
|
||||
rankedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
rerankedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
restrictFilteredCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
allTakeCandidateResults: Seq[CandidateResult[PushCandidate, Result]]
|
||||
): Future[Seq[MrRequestScribe]] = {
|
||||
if (target.isLoggedOutUser || target.isEmailUser) {
|
||||
Future.Nil
|
||||
} else if (enableCandidateFilteringScribing(target)) {
|
||||
val hydrateFeature =
|
||||
target.params(PushFeatureSwitchParams.EnableMrRequestScribingWithFeatureHydrating) ||
|
||||
target.scribeFeatureForRequestScribe
|
||||
|
||||
val candidateRequestInfoSeq = generateCandidatesScribeInfo(
|
||||
hydratedCandidates,
|
||||
preRankingFilteredCandidates,
|
||||
rankedCandidates,
|
||||
rerankedCandidates,
|
||||
restrictFilteredCandidates,
|
||||
allTakeCandidateResults,
|
||||
isFeatureHydratingEnabled = hydrateFeature
|
||||
)
|
||||
val flattenStructure =
|
||||
target.params(PushFeatureSwitchParams.EnableFlattenMrRequestScribing) || hydrateFeature
|
||||
candidateRequestInfoSeq.flatMap { candidateRequestInfos =>
|
||||
if (flattenStructure) {
|
||||
Future.collect {
|
||||
candidateRequestInfos.map { candidateRequestInfo =>
|
||||
buildScribeThrift(target, None, Some(Seq(candidateRequestInfo)))
|
||||
.map { mrRequestScribe =>
|
||||
writeAtCandidateFilteringStep(target, mrRequestScribe)
|
||||
mrRequestScribe
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildScribeThrift(target, None, Some(candidateRequestInfos))
|
||||
.map { mrRequestScribe =>
|
||||
writeAtCandidateFilteringStep(target, mrRequestScribe)
|
||||
Seq(mrRequestScribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else Future.Nil
|
||||
|
||||
}
|
||||
|
||||
private def buildScribeThrift(
|
||||
target: Target,
|
||||
targetFilteredOutPredicate: Option[String],
|
||||
candidatesRequestInfo: Option[Seq[CandidateRequestInfo]]
|
||||
): Future[MrRequestScribe] = {
|
||||
Future
|
||||
.join(
|
||||
target.targetUserState,
|
||||
generateTargetFeatureScribeInfo(target),
|
||||
target.targetUser).map {
|
||||
case (userStateOption, targetFeatureOption, gizmoduckUserOpt) =>
|
||||
val userState = userStateOption.map(userState => ThriftUserState(userState.id))
|
||||
val targetFeatures =
|
||||
targetFeatureOption.map(ScalaToJavaDataRecordConversions.javaDataRecord2ScalaDataRecord)
|
||||
val traceId = Trace.id.traceId.toLong
|
||||
|
||||
MrRequestScribe(
|
||||
requestId = UUID.randomUUID.toString.replaceAll("-", ""),
|
||||
scribedTimeMs = Time.now.inMilliseconds,
|
||||
targetUserId = target.targetId,
|
||||
targetUserInfo = Some(
|
||||
TargetUserInfo(
|
||||
userState,
|
||||
features = targetFeatures,
|
||||
userType = gizmoduckUserOpt.map(_.userType))
|
||||
),
|
||||
targetFilteredOutPredicate = targetFilteredOutPredicate,
|
||||
candidates = candidatesRequestInfo,
|
||||
traceId = Some(traceId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def generateTargetFeatureScribeInfo(
|
||||
target: Target
|
||||
): Future[Option[DataRecord]] = {
|
||||
val featureList =
|
||||
target.params(PushFeatureSwitchParams.TargetLevelFeatureListForMrRequestScribing)
|
||||
if (featureList.nonEmpty) {
|
||||
PushPredictionHelper
|
||||
.getDataRecordFromTargetFeatureMap(
|
||||
target.targetId,
|
||||
target.featureMap,
|
||||
stats
|
||||
).map { dataRecord =>
|
||||
val richRecord =
|
||||
new SRichDataRecord(dataRecord, PushQualityModelFeatureContext.featureContext)
|
||||
|
||||
val selectedRecord =
|
||||
SRichDataRecord(new DataRecord(), PushQualityModelFeatureContext.featureContext)
|
||||
featureList.map { featureName =>
|
||||
val feature: Feature[_] = {
|
||||
try {
|
||||
PushQualityModelFeatureContext.featureContext.getFeature(featureName)
|
||||
} catch {
|
||||
case _: Exception =>
|
||||
mrRequestScribeNotIncludedFeatureStats.incr()
|
||||
throw new IllegalStateException(
|
||||
"Scribing features not included in FeatureContext: " + featureName)
|
||||
}
|
||||
}
|
||||
|
||||
richRecord.getFeatureValueOpt(feature).foreach { featureVal =>
|
||||
feature.getFeatureType() match {
|
||||
case FeatureType.BINARY =>
|
||||
selectedRecord.setFeatureValue(
|
||||
feature.asInstanceOf[Feature[Boolean]],
|
||||
featureVal.asInstanceOf[Boolean])
|
||||
case FeatureType.CONTINUOUS =>
|
||||
selectedRecord.setFeatureValue(
|
||||
feature.asInstanceOf[Feature[Double]],
|
||||
featureVal.asInstanceOf[Double])
|
||||
case FeatureType.STRING =>
|
||||
selectedRecord.setFeatureValue(
|
||||
feature.asInstanceOf[Feature[String]],
|
||||
featureVal.asInstanceOf[String])
|
||||
case FeatureType.DISCRETE =>
|
||||
selectedRecord.setFeatureValue(
|
||||
feature.asInstanceOf[Feature[Long]],
|
||||
featureVal.asInstanceOf[Long])
|
||||
case _ =>
|
||||
mrRequestScribeUnsupportedFeatureTypeStats.incr()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(selectedRecord.getRecord)
|
||||
}
|
||||
} else Future.None
|
||||
}
|
||||
|
||||
private def generateCandidatesScribeInfo(
|
||||
hydratedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
preRankingFilteredCandidates: Seq[CandidateResult[PushCandidate, Result]],
|
||||
rankedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
rerankedCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
restrictFilteredCandidates: Seq[CandidateDetails[PushCandidate]],
|
||||
allTakeCandidateResults: Seq[CandidateResult[PushCandidate, Result]],
|
||||
isFeatureHydratingEnabled: Boolean
|
||||
): Future[Seq[CandidateRequestInfo]] = {
|
||||
val candidatesMap = new mutable.HashMap[String, CandidateRequestInfo]
|
||||
|
||||
hydratedCandidates.foreach { hydratedCandidate =>
|
||||
val frgNotif = hydratedCandidate.candidate.frigateNotification
|
||||
val simplifiedTweetNotificationOpt = frgNotif.tweetNotification.map { tweetNotification =>
|
||||
TweetNotification(
|
||||
tweetNotification.tweetId,
|
||||
Seq.empty[TSocialContextAction],
|
||||
tweetNotification.tweetAuthorId)
|
||||
}
|
||||
val simplifiedFrigateNotification = FrigateNotification(
|
||||
frgNotif.commonRecommendationType,
|
||||
frgNotif.notificationDisplayLocation,
|
||||
tweetNotification = simplifiedTweetNotificationOpt
|
||||
)
|
||||
candidatesMap(hydratedCandidate.candidate.impressionId) = CandidateRequestInfo(
|
||||
candidateId = "",
|
||||
candidateSource = hydratedCandidate.source.substring(
|
||||
0,
|
||||
Math.min(6, hydratedCandidate.source.length)
|
||||
),
|
||||
frigateNotification = Some(simplifiedFrigateNotification),
|
||||
modelScore = None,
|
||||
rankPosition = None,
|
||||
rerankPosition = None,
|
||||
features = None,
|
||||
isSent = Some(false)
|
||||
)
|
||||
}
|
||||
|
||||
preRankingFilteredCandidates.foreach { preRankingFilteredCandidateResult =>
|
||||
candidatesMap(preRankingFilteredCandidateResult.candidate.impressionId) =
|
||||
candidatesMap(preRankingFilteredCandidateResult.candidate.impressionId)
|
||||
.copy(
|
||||
candidateFilteredOutPredicate = preRankingFilteredCandidateResult.result match {
|
||||
case Invalid(reason) => reason
|
||||
case _ => {
|
||||
mrRequestScribeInvalidStats.incr()
|
||||
throw new IllegalStateException(
|
||||
"Invalid reason for Candidate Filtering " + preRankingFilteredCandidateResult.result)
|
||||
}
|
||||
},
|
||||
candidateFilteredOutStep = Some(CandidateFilteredOutStep.PreRankFiltering)
|
||||
)
|
||||
}
|
||||
|
||||
for {
|
||||
_ <- Future.collectToTry {
|
||||
rankedCandidates.zipWithIndex.map {
|
||||
case (rankedCandidateDetail, index) =>
|
||||
val modelScoresFut = {
|
||||
val crt = rankedCandidateDetail.candidate.commonRecType
|
||||
if (RecTypes.notEligibleForModelScoreTracking.contains(crt)) Future.None
|
||||
else rankedCandidateDetail.candidate.modelScores.map(Some(_))
|
||||
}
|
||||
|
||||
modelScoresFut.map { modelScores =>
|
||||
candidatesMap(rankedCandidateDetail.candidate.impressionId) =
|
||||
candidatesMap(rankedCandidateDetail.candidate.impressionId).copy(
|
||||
rankPosition = Some(index),
|
||||
modelScore = modelScores
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = rerankedCandidates.zipWithIndex.foreach {
|
||||
case (rerankedCandidateDetail, index) => {
|
||||
candidatesMap(rerankedCandidateDetail.candidate.impressionId) =
|
||||
candidatesMap(rerankedCandidateDetail.candidate.impressionId).copy(
|
||||
rerankPosition = Some(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_ <- Future.collectToTry {
|
||||
rerankedCandidates.map { rerankedCandidateDetail =>
|
||||
if (isFeatureHydratingEnabled) {
|
||||
PushPredictionHelper
|
||||
.getDataRecord(
|
||||
rerankedCandidateDetail.candidate.target.targetHydrationContext,
|
||||
rerankedCandidateDetail.candidate.target.featureMap,
|
||||
rerankedCandidateDetail.candidate.candidateHydrationContext,
|
||||
rerankedCandidateDetail.candidate.candidateFeatureMap(),
|
||||
stats
|
||||
).map { features =>
|
||||
candidatesMap(rerankedCandidateDetail.candidate.impressionId) =
|
||||
candidatesMap(rerankedCandidateDetail.candidate.impressionId).copy(
|
||||
features = Some(
|
||||
ScalaToJavaDataRecordConversions.javaDataRecord2ScalaDataRecord(features))
|
||||
)
|
||||
}
|
||||
} else Future.Unit
|
||||
}
|
||||
}
|
||||
|
||||
_ = restrictFilteredCandidates.foreach { restrictFilteredCandidateDetatil =>
|
||||
candidatesMap(restrictFilteredCandidateDetatil.candidate.impressionId) =
|
||||
candidatesMap(restrictFilteredCandidateDetatil.candidate.impressionId)
|
||||
.copy(candidateFilteredOutStep = Some(CandidateFilteredOutStep.Restrict))
|
||||
}
|
||||
|
||||
_ = allTakeCandidateResults.foreach { allTakeCandidateResult =>
|
||||
allTakeCandidateResult.result match {
|
||||
case OK =>
|
||||
candidatesMap(allTakeCandidateResult.candidate.impressionId) =
|
||||
candidatesMap(allTakeCandidateResult.candidate.impressionId).copy(isSent = Some(true))
|
||||
case Invalid(reason) =>
|
||||
candidatesMap(allTakeCandidateResult.candidate.impressionId) =
|
||||
candidatesMap(allTakeCandidateResult.candidate.impressionId).copy(
|
||||
candidateFilteredOutPredicate = reason,
|
||||
candidateFilteredOutStep = Some(CandidateFilteredOutStep.PostRankFiltering))
|
||||
case _ =>
|
||||
mrRequestScribeInvalidStats.incr()
|
||||
throw new IllegalStateException(
|
||||
"Invalid reason for Candidate Filtering " + allTakeCandidateResult.result)
|
||||
}
|
||||
}
|
||||
} yield candidatesMap.values.toSeq
|
||||
}
|
||||
|
||||
private def enableTargetFilteringScribing(target: Target): Boolean = {
|
||||
target.params(PushParams.EnableMrRequestScribing) && target.params(
|
||||
PushFeatureSwitchParams.EnableMrRequestScribingForTargetFiltering)
|
||||
}
|
||||
|
||||
private def enableCandidateFilteringScribing(target: Target): Boolean = {
|
||||
target.params(PushParams.EnableMrRequestScribing) && target.params(
|
||||
PushFeatureSwitchParams.EnableMrRequestScribingForCandidateFiltering)
|
||||
}
|
||||
|
||||
private def writeAtTargetFilteringStep(target: Target, mrRequestScribe: MrRequestScribe) = {
|
||||
logToScribe(mrRequestScribe)
|
||||
mrRequestScribeTargetFilteringStats.incr()
|
||||
}
|
||||
|
||||
private def writeAtCandidateFilteringStep(target: Target, mrRequestScribe: MrRequestScribe) = {
|
||||
logToScribe(mrRequestScribe)
|
||||
mrRequestScribeCandidateFilteringStats.incr()
|
||||
}
|
||||
|
||||
private def logToScribe(mrRequestScribe: MrRequestScribe): Unit = {
|
||||
val logEntry: String = MrRequestScribeInjection(mrRequestScribe)
|
||||
mrRequestScribeLogger.info(logEntry)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,250 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler
|
||||
|
||||
import com.twitter.finagle.stats.BroadcastStatsReceiver
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base.CandidateDetails
|
||||
import com.twitter.frigate.common.base.CandidateFilteringOnlyFlow
|
||||
import com.twitter.frigate.common.base.CandidateResult
|
||||
import com.twitter.frigate.common.base.FeatureMap
|
||||
import com.twitter.frigate.common.base.OK
|
||||
import com.twitter.frigate.common.base.Response
|
||||
import com.twitter.frigate.common.base.Result
|
||||
import com.twitter.frigate.common.base.Stats.track
|
||||
import com.twitter.frigate.common.config.CommonConstants
|
||||
import com.twitter.frigate.common.logger.MRLogger
|
||||
import com.twitter.frigate.common.rec_types.RecTypes
|
||||
import com.twitter.frigate.common.util.InvalidRequestException
|
||||
import com.twitter.frigate.common.util.MrNtabCopyObjects
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.config.Config
|
||||
import com.twitter.frigate.pushservice.ml.HydrationContextBuilder
|
||||
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams.EnableMagicFanoutNewsForYouNtabCopy
|
||||
import com.twitter.frigate.pushservice.scriber.MrRequestScribeHandler
|
||||
import com.twitter.frigate.pushservice.send_handler.generator.PushRequestToCandidate
|
||||
import com.twitter.frigate.pushservice.take.SendHandlerNotifier
|
||||
import com.twitter.frigate.pushservice.take.candidate_validator.SendHandlerPostCandidateValidator
|
||||
import com.twitter.frigate.pushservice.take.candidate_validator.SendHandlerPreCandidateValidator
|
||||
import com.twitter.frigate.pushservice.target.PushTargetUserBuilder
|
||||
import com.twitter.frigate.pushservice.util.ResponseStatsTrackUtils.trackStatsForResponseToRequest
|
||||
import com.twitter.frigate.pushservice.util.SendHandlerPredicateUtil
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushRequest
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushRequestScribe
|
||||
import com.twitter.frigate.pushservice.thriftscala.PushResponse
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.nrel.heavyranker.FeatureHydrator
|
||||
import com.twitter.util._
|
||||
|
||||
/**
|
||||
* A handler for sending PushRequests
|
||||
*/
|
||||
class SendHandler(
|
||||
pushTargetUserBuilder: PushTargetUserBuilder,
|
||||
preCandidateValidator: SendHandlerPreCandidateValidator,
|
||||
postCandidateValidator: SendHandlerPostCandidateValidator,
|
||||
sendHandlerNotifier: SendHandlerNotifier,
|
||||
candidateHydrator: SendHandlerPushCandidateHydrator,
|
||||
featureHydrator: FeatureHydrator,
|
||||
sendHandlerPredicateUtil: SendHandlerPredicateUtil,
|
||||
mrRequestScriberNode: String
|
||||
)(
|
||||
implicit val statsReceiver: StatsReceiver,
|
||||
implicit val config: Config)
|
||||
extends CandidateFilteringOnlyFlow[Target, RawCandidate, PushCandidate] {
|
||||
|
||||
implicit private val timer: Timer = new JavaTimer(true)
|
||||
val stats = statsReceiver.scope("SendHandler")
|
||||
val log = MRLogger("SendHandler")
|
||||
|
||||
private val buildTargetStats = stats.scope("build_target")
|
||||
|
||||
private val candidateHydrationLatency: Stat =
|
||||
stats.stat("candidateHydrationLatency")
|
||||
|
||||
private val candidatePreValidatorLatency: Stat =
|
||||
stats.stat("candidatePreValidatorLatency")
|
||||
|
||||
private val candidatePostValidatorLatency: Stat =
|
||||
stats.stat("candidatePostValidatorLatency")
|
||||
|
||||
private val featureHydrationLatency: StatsReceiver =
|
||||
stats.scope("featureHydrationLatency")
|
||||
|
||||
private val mrRequestScribeHandler =
|
||||
new MrRequestScribeHandler(mrRequestScriberNode, stats.scope("mr_request_scribe"))
|
||||
|
||||
def apply(request: PushRequest): Future[PushResponse] = {
|
||||
val receivers = Seq(
|
||||
stats,
|
||||
stats.scope(request.notification.commonRecommendationType.toString)
|
||||
)
|
||||
val bStats = BroadcastStatsReceiver(receivers)
|
||||
bStats.counter("requests").incr()
|
||||
Stat
|
||||
.timeFuture(bStats.stat("latency"))(
|
||||
process(request).raiseWithin(CommonConstants.maxPushRequestDuration))
|
||||
.onSuccess {
|
||||
case (pushResp, rawCandidate) =>
|
||||
trackStatsForResponseToRequest(
|
||||
rawCandidate.commonRecType,
|
||||
rawCandidate.target,
|
||||
pushResp,
|
||||
receivers)(statsReceiver)
|
||||
if (!request.context.exists(_.darkWrite.contains(true))) {
|
||||
config.requestScribe(PushRequestScribe(request, pushResp))
|
||||
}
|
||||
}
|
||||
.onFailure { ex =>
|
||||
bStats.counter("failures").incr()
|
||||
bStats.scope("failures").counter(ex.getClass.getCanonicalName).incr()
|
||||
}
|
||||
.map {
|
||||
case (pushResp, _) => pushResp
|
||||
}
|
||||
}
|
||||
|
||||
private def process(request: PushRequest): Future[(PushResponse, RawCandidate)] = {
|
||||
val recType = request.notification.commonRecommendationType
|
||||
|
||||
track(buildTargetStats)(
|
||||
pushTargetUserBuilder
|
||||
.buildTarget(
|
||||
request.userId,
|
||||
request.context
|
||||
)
|
||||
).flatMap { targetUser =>
|
||||
val responseWithScribedInfo = request.context.exists { context =>
|
||||
context.responseWithScribedInfo.contains(true)
|
||||
}
|
||||
val newRequest =
|
||||
if (request.notification.commonRecommendationType == CommonRecommendationType.MagicFanoutNewsEvent &&
|
||||
targetUser.params(EnableMagicFanoutNewsForYouNtabCopy)) {
|
||||
val newNotification = request.notification.copy(ntabCopyId =
|
||||
Some(MrNtabCopyObjects.MagicFanoutNewsForYouCopy.copyId))
|
||||
request.copy(notification = newNotification)
|
||||
} else request
|
||||
|
||||
if (RecTypes.isSendHandlerType(recType) || newRequest.context.exists(
|
||||
_.allowCRT.contains(true))) {
|
||||
|
||||
val rawCandidateFut = PushRequestToCandidate.generatePushCandidate(
|
||||
newRequest.notification,
|
||||
targetUser
|
||||
)
|
||||
|
||||
rawCandidateFut.flatMap { rawCandidate =>
|
||||
val pushResponse = process(targetUser, Seq(rawCandidate)).flatMap {
|
||||
sendHandlerNotifier.checkResponseAndNotify(_, responseWithScribedInfo)
|
||||
}
|
||||
|
||||
pushResponse.map { pushResponse =>
|
||||
(pushResponse, rawCandidate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Future.exception(InvalidRequestException(s"${recType.name} not supported in SendHandler"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def hydrateFeatures(
|
||||
candidateDetails: Seq[CandidateDetails[PushCandidate]],
|
||||
target: Target,
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
candidateDetails.headOption match {
|
||||
case Some(candidateDetail)
|
||||
if RecTypes.notEligibleForModelScoreTracking(candidateDetail.candidate.commonRecType) =>
|
||||
Future.value(candidateDetails)
|
||||
|
||||
case Some(candidateDetail) =>
|
||||
val hydrationContextFut = HydrationContextBuilder.build(candidateDetail.candidate)
|
||||
hydrationContextFut.flatMap { hc =>
|
||||
featureHydrator
|
||||
.hydrateCandidate(Seq(hc), target.mrRequestContextForFeatureStore)
|
||||
.map { hydrationResult =>
|
||||
val features = hydrationResult.getOrElse(hc, FeatureMap())
|
||||
candidateDetail.candidate.mergeFeatures(features)
|
||||
candidateDetails
|
||||
}
|
||||
}
|
||||
case _ => Future.Nil
|
||||
}
|
||||
}
|
||||
|
||||
override def process(
|
||||
target: Target,
|
||||
externalCandidates: Seq[RawCandidate]
|
||||
): Future[Response[PushCandidate, Result]] = {
|
||||
val candidate = externalCandidates.map(CandidateDetails(_, "realtime"))
|
||||
|
||||
for {
|
||||
hydratedCandidatesWithCopy <- hydrateCandidates(candidate)
|
||||
|
||||
(candidates, preHydrationFilteredCandidates) <- track(filterStats)(
|
||||
filter(target, hydratedCandidatesWithCopy)
|
||||
)
|
||||
|
||||
featureHydratedCandidates <-
|
||||
track(featureHydrationLatency)(hydrateFeatures(candidates, target))
|
||||
|
||||
allTakeCandidateResults <- track(takeStats)(
|
||||
take(target, featureHydratedCandidates, desiredCandidateCount(target))
|
||||
)
|
||||
|
||||
_ <- mrRequestScribeHandler.scribeForCandidateFiltering(
|
||||
target = target,
|
||||
hydratedCandidates = hydratedCandidatesWithCopy,
|
||||
preRankingFilteredCandidates = preHydrationFilteredCandidates,
|
||||
rankedCandidates = featureHydratedCandidates,
|
||||
rerankedCandidates = Seq.empty,
|
||||
restrictFilteredCandidates = Seq.empty, // no restrict step
|
||||
allTakeCandidateResults = allTakeCandidateResults
|
||||
)
|
||||
} yield {
|
||||
|
||||
/**
|
||||
* We combine the results for all filtering steps and pass on in sequence to next step
|
||||
*
|
||||
* This is done to ensure the filtering reason for the candidate from multiple levels of
|
||||
* filtering is carried all the way until [[PushResponse]] is built and returned from
|
||||
* frigate-pushservice-send
|
||||
*/
|
||||
Response(OK, allTakeCandidateResults ++ preHydrationFilteredCandidates)
|
||||
}
|
||||
}
|
||||
|
||||
override def hydrateCandidates(
|
||||
candidates: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
Stat.timeFuture(candidateHydrationLatency)(candidateHydrator(candidates))
|
||||
}
|
||||
|
||||
// Filter Step - pre-predicates and app specific predicates
|
||||
override def filter(
|
||||
target: Target,
|
||||
hydratedCandidatesDetails: Seq[CandidateDetails[PushCandidate]]
|
||||
): Future[
|
||||
(Seq[CandidateDetails[PushCandidate]], Seq[CandidateResult[PushCandidate, Result]])
|
||||
] = {
|
||||
Stat.timeFuture(candidatePreValidatorLatency)(
|
||||
sendHandlerPredicateUtil.preValidationForCandidate(
|
||||
hydratedCandidatesDetails,
|
||||
preCandidateValidator
|
||||
))
|
||||
}
|
||||
|
||||
// Post Validation - Take step
|
||||
override def validCandidates(
|
||||
target: Target,
|
||||
candidates: Seq[PushCandidate]
|
||||
): Future[Seq[Result]] = {
|
||||
Stat.timeFuture(candidatePostValidatorLatency)(Future.collect(candidates.map { candidate =>
|
||||
sendHandlerPredicateUtil
|
||||
.postValidationForCandidate(candidate, postCandidateValidator)
|
||||
.map(res => res.result)
|
||||
}))
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,184 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler
|
||||
|
||||
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.base._
|
||||
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||
import com.twitter.frigate.common.util.MrNtabCopyObjects
|
||||
import com.twitter.frigate.common.util.MrPushCopyObjects
|
||||
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||
import com.twitter.frigate.pushservice.store.EventRequest
|
||||
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil._
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||
import com.twitter.interests.thriftscala.UserInterests
|
||||
import com.twitter.livevideo.timeline.domain.v2.{Event => LiveEvent}
|
||||
import com.twitter.simclusters_v2.thriftscala.SimClustersInferredEntities
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.strato.client.UserId
|
||||
import com.twitter.ubs.thriftscala.AudioSpace
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class SendHandlerPushCandidateHydrator(
|
||||
lexServiceStore: ReadableStore[EventRequest, LiveEvent],
|
||||
fanoutMetadataStore: ReadableStore[(Long, Long), FanoutEvent],
|
||||
semanticCoreMegadataStore: ReadableStore[SemanticEntityForQuery, EntityMegadata],
|
||||
safeUserStore: ReadableStore[Long, User],
|
||||
simClusterToEntityStore: ReadableStore[Int, SimClustersInferredEntities],
|
||||
audioSpaceStore: ReadableStore[String, AudioSpace],
|
||||
interestsLookupStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||
uttEntityHydrationStore: UttEntityHydrationStore,
|
||||
superFollowCreatorTweetCountStore: ReadableStore[UserId, Int]
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver,
|
||||
implicit val weightedOpenOrNtabClickModelScorer: PushMLModelScorer) {
|
||||
|
||||
lazy val candidateWithCopyNumStat = statsReceiver.stat("candidate_with_copy_num")
|
||||
lazy val hydratedCandidateStat = statsReceiver.scope("hydrated_candidates")
|
||||
|
||||
def updateCandidates(
|
||||
candidateDetails: Seq[CandidateDetails[RawCandidate]],
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
|
||||
Future.collect {
|
||||
candidateDetails.map { candidateDetail =>
|
||||
val pushCandidate = candidateDetail.candidate
|
||||
|
||||
val copyIds = getCopyIdsByCRT(pushCandidate.commonRecType)
|
||||
|
||||
val hydratedCandidateFut = pushCandidate match {
|
||||
case magicFanoutNewsEventCandidate: MagicFanoutNewsEventCandidate =>
|
||||
getHydratedCandidateForMagicFanoutNewsEvent(
|
||||
magicFanoutNewsEventCandidate,
|
||||
copyIds,
|
||||
lexServiceStore,
|
||||
fanoutMetadataStore,
|
||||
semanticCoreMegadataStore,
|
||||
simClusterToEntityStore,
|
||||
interestsLookupStore,
|
||||
uttEntityHydrationStore
|
||||
)
|
||||
|
||||
case scheduledSpaceSubscriberCandidate: ScheduledSpaceSubscriberCandidate =>
|
||||
getHydratedCandidateForScheduledSpaceSubscriber(
|
||||
scheduledSpaceSubscriberCandidate,
|
||||
safeUserStore,
|
||||
copyIds,
|
||||
audioSpaceStore
|
||||
)
|
||||
case scheduledSpaceSpeakerCandidate: ScheduledSpaceSpeakerCandidate =>
|
||||
getHydratedCandidateForScheduledSpaceSpeaker(
|
||||
scheduledSpaceSpeakerCandidate,
|
||||
safeUserStore,
|
||||
copyIds,
|
||||
audioSpaceStore
|
||||
)
|
||||
case magicFanoutSportsEventCandidate: MagicFanoutSportsEventCandidate with MagicFanoutSportsScoreInformation =>
|
||||
getHydratedCandidateForMagicFanoutSportsEvent(
|
||||
magicFanoutSportsEventCandidate,
|
||||
copyIds,
|
||||
lexServiceStore,
|
||||
fanoutMetadataStore,
|
||||
semanticCoreMegadataStore,
|
||||
interestsLookupStore,
|
||||
uttEntityHydrationStore
|
||||
)
|
||||
case magicFanoutProductLaunchCandidate: MagicFanoutProductLaunchCandidate =>
|
||||
getHydratedCandidateForMagicFanoutProductLaunch(
|
||||
magicFanoutProductLaunchCandidate,
|
||||
copyIds)
|
||||
case creatorEventCandidate: MagicFanoutCreatorEventCandidate =>
|
||||
getHydratedCandidateForMagicFanoutCreatorEvent(
|
||||
creatorEventCandidate,
|
||||
safeUserStore,
|
||||
copyIds,
|
||||
superFollowCreatorTweetCountStore)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException("Incorrect candidate type when update candidates")
|
||||
}
|
||||
|
||||
hydratedCandidateFut.map { hydratedCandidate =>
|
||||
hydratedCandidateStat.counter(hydratedCandidate.commonRecType.name).incr()
|
||||
CandidateDetails(
|
||||
hydratedCandidate,
|
||||
source = candidateDetail.source
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def getCopyIdsByCRT(crt: CommonRecommendationType): CopyIds = {
|
||||
crt match {
|
||||
case CommonRecommendationType.MagicFanoutNewsEvent =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.MagicFanoutNewsPushCopy.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.MagicFanoutNewsForYouCopy.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
|
||||
case CommonRecommendationType.ScheduledSpaceSubscriber =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.ScheduledSpaceSubscriber.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.ScheduledSpaceSubscriber.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.ScheduledSpaceSpeaker =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.ScheduledSpaceSpeaker.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.ScheduledSpaceSpeakerNow.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.SpaceSpeaker =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.SpaceSpeaker.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.SpaceSpeaker.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.SpaceHost =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.SpaceHost.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.SpaceHost.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.MagicFanoutSportsEvent =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.MagicFanoutSportsPushCopy.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.MagicFanoutSportsCopy.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.MagicFanoutProductLaunch =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.MagicFanoutProductLaunch.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.ProductLaunch.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.CreatorSubscriber =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.MagicFanoutCreatorSubscription.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.MagicFanoutCreatorSubscription.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case CommonRecommendationType.NewCreator =>
|
||||
CopyIds(
|
||||
pushCopyId = Some(MrPushCopyObjects.MagicFanoutNewCreator.copyId),
|
||||
ntabCopyId = Some(MrNtabCopyObjects.MagicFanoutNewCreator.copyId),
|
||||
aggregationId = None
|
||||
)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException("Incorrect candidate type when fetch copy ids")
|
||||
}
|
||||
}
|
||||
|
||||
def apply(
|
||||
candidateDetails: Seq[CandidateDetails[RawCandidate]]
|
||||
): Future[Seq[CandidateDetails[PushCandidate]]] = {
|
||||
updateCandidates(candidateDetails)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,17 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.util.Future
|
||||
|
||||
trait CandidateGenerator {
|
||||
|
||||
/**
|
||||
* Build RawCandidate from FrigateNotification
|
||||
* @param target
|
||||
* @param frigateNotification
|
||||
* @return RawCandidate
|
||||
*/
|
||||
def getCandidate(target: Target, frigateNotification: FrigateNotification): Future[RawCandidate]
|
||||
}
|
Binary file not shown.
@ -1,70 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.common.base.MagicFanoutCreatorEventCandidate
|
||||
import com.twitter.frigate.magic_events.thriftscala.CreatorFanoutType
|
||||
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.model.PushTypes
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.util.Future
|
||||
|
||||
object MagicFanoutCreatorEventCandidateGenerator extends CandidateGenerator {
|
||||
override def getCandidate(
|
||||
targetUser: PushTypes.Target,
|
||||
notification: FrigateNotification
|
||||
): Future[PushTypes.RawCandidate] = {
|
||||
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.CreatorSubscriber || notification.commonRecommendationType == CommonRecommendationType.NewCreator,
|
||||
"MagicFanoutCreatorEvent: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
require(
|
||||
notification.creatorSubscriptionNotification.isDefined,
|
||||
"MagicFanoutCreatorEvent: creatorSubscriptionNotification is not defined")
|
||||
require(
|
||||
notification.creatorSubscriptionNotification.exists(_.magicFanoutPushId.isDefined),
|
||||
"MagicFanoutCreatorEvent: magicFanoutPushId is not defined")
|
||||
require(
|
||||
notification.creatorSubscriptionNotification.exists(_.fanoutReasons.isDefined),
|
||||
"MagicFanoutCreatorEvent: fanoutReasons is not defined")
|
||||
require(
|
||||
notification.creatorSubscriptionNotification.exists(_.creatorId.isDefined),
|
||||
"MagicFanoutCreatorEvent: creatorId is not defined")
|
||||
if (notification.commonRecommendationType == CommonRecommendationType.CreatorSubscriber) {
|
||||
require(
|
||||
notification.creatorSubscriptionNotification
|
||||
.exists(_.subscriberId.isDefined),
|
||||
"MagicFanoutCreatorEvent: subscriber id is not defined"
|
||||
)
|
||||
}
|
||||
|
||||
val creatorSubscriptionNotification = notification.creatorSubscriptionNotification.get
|
||||
|
||||
val candidate = new RawCandidate with MagicFanoutCreatorEventCandidate {
|
||||
|
||||
override val target: Target = targetUser
|
||||
|
||||
override val pushId: Long =
|
||||
creatorSubscriptionNotification.magicFanoutPushId.get
|
||||
|
||||
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||
creatorSubscriptionNotification.fanoutReasons.get
|
||||
|
||||
override val creatorFanoutType: CreatorFanoutType =
|
||||
creatorSubscriptionNotification.creatorFanoutType
|
||||
|
||||
override val commonRecType: CommonRecommendationType =
|
||||
notification.commonRecommendationType
|
||||
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
|
||||
override val subscriberId: Option[Long] = creatorSubscriptionNotification.subscriberId
|
||||
|
||||
override val creatorId: Long = creatorSubscriptionNotification.creatorId.get
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,57 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.common.base.MagicFanoutNewsEventCandidate
|
||||
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.frigate.thriftscala.MagicFanoutEventNotificationDetails
|
||||
import com.twitter.util.Future
|
||||
|
||||
object MagicFanoutNewsEventCandidateGenerator extends CandidateGenerator {
|
||||
|
||||
override def getCandidate(
|
||||
targetUser: Target,
|
||||
notification: FrigateNotification
|
||||
): Future[RawCandidate] = {
|
||||
|
||||
/**
|
||||
* frigateNotification recommendation type should be [[CommonRecommendationType.MagicFanoutNewsEvent]]
|
||||
* AND pushId field should be set
|
||||
**/
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.MagicFanoutNewsEvent,
|
||||
"MagicFanoutNewsEvent: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
|
||||
require(
|
||||
notification.magicFanoutEventNotification.exists(_.pushId.isDefined),
|
||||
"MagicFanoutNewsEvent: pushId is not defined")
|
||||
|
||||
val magicFanoutEventNotification = notification.magicFanoutEventNotification.get
|
||||
|
||||
val candidate = new RawCandidate with MagicFanoutNewsEventCandidate {
|
||||
|
||||
override val target: Target = targetUser
|
||||
|
||||
override val eventId: Long = magicFanoutEventNotification.eventId
|
||||
|
||||
override val pushId: Long = magicFanoutEventNotification.pushId.get
|
||||
|
||||
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||
magicFanoutEventNotification.eventReasons.getOrElse(Seq.empty)
|
||||
|
||||
override val momentId: Option[Long] = magicFanoutEventNotification.momentId
|
||||
|
||||
override val eventLanguage: Option[String] = magicFanoutEventNotification.eventLanguage
|
||||
|
||||
override val details: Option[MagicFanoutEventNotificationDetails] =
|
||||
magicFanoutEventNotification.details
|
||||
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,54 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.common.base.MagicFanoutProductLaunchCandidate
|
||||
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||
import com.twitter.frigate.magic_events.thriftscala.ProductType
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.model.PushTypes
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.util.Future
|
||||
|
||||
object MagicFanoutProductLaunchCandidateGenerator extends CandidateGenerator {
|
||||
|
||||
override def getCandidate(
|
||||
targetUser: PushTypes.Target,
|
||||
notification: FrigateNotification
|
||||
): Future[PushTypes.RawCandidate] = {
|
||||
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.MagicFanoutProductLaunch,
|
||||
"MagicFanoutProductLaunch: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
require(
|
||||
notification.magicFanoutProductLaunchNotification.isDefined,
|
||||
"MagicFanoutProductLaunch: magicFanoutProductLaunchNotification is not defined")
|
||||
require(
|
||||
notification.magicFanoutProductLaunchNotification.exists(_.magicFanoutPushId.isDefined),
|
||||
"MagicFanoutProductLaunch: magicFanoutPushId is not defined")
|
||||
require(
|
||||
notification.magicFanoutProductLaunchNotification.exists(_.fanoutReasons.isDefined),
|
||||
"MagicFanoutProductLaunch: fanoutReasons is not defined")
|
||||
|
||||
val magicFanoutProductLaunchNotification = notification.magicFanoutProductLaunchNotification.get
|
||||
|
||||
val candidate = new RawCandidate with MagicFanoutProductLaunchCandidate {
|
||||
|
||||
override val target: Target = targetUser
|
||||
|
||||
override val pushId: Long =
|
||||
magicFanoutProductLaunchNotification.magicFanoutPushId.get
|
||||
|
||||
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||
magicFanoutProductLaunchNotification.fanoutReasons.get
|
||||
|
||||
override val productLaunchType: ProductType =
|
||||
magicFanoutProductLaunchNotification.productLaunchType
|
||||
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,153 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.datatools.entityservice.entities.sports.thriftscala.BaseballGameLiveUpdate
|
||||
import com.twitter.datatools.entityservice.entities.sports.thriftscala.BasketballGameLiveUpdate
|
||||
import com.twitter.datatools.entityservice.entities.sports.thriftscala.CricketMatchLiveUpdate
|
||||
import com.twitter.datatools.entityservice.entities.sports.thriftscala.NflFootballGameLiveUpdate
|
||||
import com.twitter.datatools.entityservice.entities.sports.thriftscala.SoccerMatchLiveUpdate
|
||||
import com.twitter.escherbird.common.thriftscala.Domains
|
||||
import com.twitter.escherbird.common.thriftscala.QualifiedId
|
||||
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||
import com.twitter.frigate.common.base.BaseGameScore
|
||||
import com.twitter.frigate.common.base.MagicFanoutSportsEventCandidate
|
||||
import com.twitter.frigate.common.base.MagicFanoutSportsScoreInformation
|
||||
import com.twitter.frigate.common.base.TeamInfo
|
||||
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||
import com.twitter.frigate.pushservice.exception.InvalidSportDomainException
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.params.PushConstants
|
||||
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutSportsUtil
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.frigate.thriftscala.MagicFanoutEventNotificationDetails
|
||||
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
object MagicFanoutSportsEventCandidateGenerator {
|
||||
|
||||
final def getCandidate(
|
||||
targetUser: Target,
|
||||
notification: FrigateNotification,
|
||||
basketballGameScoreStore: ReadableStore[QualifiedId, BasketballGameLiveUpdate],
|
||||
baseballGameScoreStore: ReadableStore[QualifiedId, BaseballGameLiveUpdate],
|
||||
cricketMatchScoreStore: ReadableStore[QualifiedId, CricketMatchLiveUpdate],
|
||||
soccerMatchScoreStore: ReadableStore[QualifiedId, SoccerMatchLiveUpdate],
|
||||
nflGameScoreStore: ReadableStore[QualifiedId, NflFootballGameLiveUpdate],
|
||||
semanticCoreMegadataStore: ReadableStore[SemanticEntityForQuery, EntityMegadata],
|
||||
): Future[RawCandidate] = {
|
||||
|
||||
/**
|
||||
* frigateNotification recommendation type should be [[CommonRecommendationType.MagicFanoutSportsEvent]]
|
||||
* AND pushId field should be set
|
||||
*
|
||||
* */
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.MagicFanoutSportsEvent,
|
||||
"MagicFanoutSports: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
|
||||
require(
|
||||
notification.magicFanoutEventNotification.exists(_.pushId.isDefined),
|
||||
"MagicFanoutSportsEvent: pushId is not defined")
|
||||
|
||||
val magicFanoutEventNotification = notification.magicFanoutEventNotification.get
|
||||
val eventId = magicFanoutEventNotification.eventId
|
||||
val _isScoreUpdate = magicFanoutEventNotification.isScoreUpdate.getOrElse(false)
|
||||
|
||||
val gameScoresFut: Future[Option[BaseGameScore]] = {
|
||||
if (_isScoreUpdate) {
|
||||
semanticCoreMegadataStore
|
||||
.get(SemanticEntityForQuery(PushConstants.SportsEventDomainId, eventId))
|
||||
.flatMap {
|
||||
case Some(megadata) =>
|
||||
if (megadata.domains.contains(Domains.BasketballGame)) {
|
||||
basketballGameScoreStore
|
||||
.get(QualifiedId(Domains.BasketballGame.value, eventId)).map {
|
||||
case Some(game) if game.status.isDefined =>
|
||||
val status = game.status.get
|
||||
MagicFanoutSportsUtil.transformToGameScore(game.score, status)
|
||||
case _ => None
|
||||
}
|
||||
} else if (megadata.domains.contains(Domains.BaseballGame)) {
|
||||
baseballGameScoreStore
|
||||
.get(QualifiedId(Domains.BaseballGame.value, eventId)).map {
|
||||
case Some(game) if game.status.isDefined =>
|
||||
val status = game.status.get
|
||||
MagicFanoutSportsUtil.transformToGameScore(game.runs, status)
|
||||
case _ => None
|
||||
}
|
||||
} else if (megadata.domains.contains(Domains.NflFootballGame)) {
|
||||
nflGameScoreStore
|
||||
.get(QualifiedId(Domains.NflFootballGame.value, eventId)).map {
|
||||
case Some(game) if game.status.isDefined =>
|
||||
val nflScore = MagicFanoutSportsUtil.transformNFLGameScore(game)
|
||||
nflScore
|
||||
case _ => None
|
||||
}
|
||||
} else if (megadata.domains.contains(Domains.SoccerMatch)) {
|
||||
soccerMatchScoreStore
|
||||
.get(QualifiedId(Domains.SoccerMatch.value, eventId)).map {
|
||||
case Some(game) if game.status.isDefined =>
|
||||
val soccerScore = MagicFanoutSportsUtil.transformSoccerGameScore(game)
|
||||
soccerScore
|
||||
case _ => None
|
||||
}
|
||||
} else {
|
||||
// The domains are not in our list of supported sports
|
||||
throw new InvalidSportDomainException(
|
||||
s"Domain for entity ${eventId} is not supported")
|
||||
}
|
||||
case _ => Future.None
|
||||
}
|
||||
} else Future.None
|
||||
}
|
||||
|
||||
val homeTeamInfoFut: Future[Option[TeamInfo]] = gameScoresFut.flatMap {
|
||||
case Some(gameScore) =>
|
||||
MagicFanoutSportsUtil.getTeamInfo(gameScore.home, semanticCoreMegadataStore)
|
||||
case _ => Future.None
|
||||
}
|
||||
|
||||
val awayTeamInfoFut: Future[Option[TeamInfo]] = gameScoresFut.flatMap {
|
||||
case Some(gameScore) =>
|
||||
MagicFanoutSportsUtil.getTeamInfo(gameScore.away, semanticCoreMegadataStore)
|
||||
case _ => Future.None
|
||||
}
|
||||
|
||||
val candidate = new RawCandidate
|
||||
with MagicFanoutSportsEventCandidate
|
||||
with MagicFanoutSportsScoreInformation {
|
||||
|
||||
override val target: Target = targetUser
|
||||
|
||||
override val eventId: Long = magicFanoutEventNotification.eventId
|
||||
|
||||
override val pushId: Long = magicFanoutEventNotification.pushId.get
|
||||
|
||||
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||
magicFanoutEventNotification.eventReasons.getOrElse(Seq.empty)
|
||||
|
||||
override val momentId: Option[Long] = magicFanoutEventNotification.momentId
|
||||
|
||||
override val eventLanguage: Option[String] = magicFanoutEventNotification.eventLanguage
|
||||
|
||||
override val details: Option[MagicFanoutEventNotificationDetails] =
|
||||
magicFanoutEventNotification.details
|
||||
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
|
||||
override val homeTeamInfo: Future[Option[TeamInfo]] = homeTeamInfoFut
|
||||
|
||||
override val awayTeamInfo: Future[Option[TeamInfo]] = awayTeamInfoFut
|
||||
|
||||
override val gameScores: Future[Option[BaseGameScore]] = gameScoresFut
|
||||
|
||||
override val isScoreUpdate: Boolean = _isScoreUpdate
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,49 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.pushservice.config.Config
|
||||
import com.twitter.frigate.pushservice.exception.UnsupportedCrtException
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.frigate.thriftscala.{CommonRecommendationType => CRT}
|
||||
import com.twitter.util.Future
|
||||
|
||||
object PushRequestToCandidate {
|
||||
final def generatePushCandidate(
|
||||
frigateNotification: FrigateNotification,
|
||||
target: Target
|
||||
)(
|
||||
implicit config: Config
|
||||
): Future[RawCandidate] = {
|
||||
|
||||
val candidateGenerator: (Target, FrigateNotification) => Future[RawCandidate] = {
|
||||
frigateNotification.commonRecommendationType match {
|
||||
case CRT.MagicFanoutNewsEvent => MagicFanoutNewsEventCandidateGenerator.getCandidate
|
||||
case CRT.ScheduledSpaceSubscriber => ScheduledSpaceSubscriberCandidateGenerator.getCandidate
|
||||
case CRT.ScheduledSpaceSpeaker => ScheduledSpaceSpeakerCandidateGenerator.getCandidate
|
||||
case CRT.MagicFanoutSportsEvent =>
|
||||
MagicFanoutSportsEventCandidateGenerator.getCandidate(
|
||||
_,
|
||||
_,
|
||||
config.basketballGameScoreStore,
|
||||
config.baseballGameScoreStore,
|
||||
config.cricketMatchScoreStore,
|
||||
config.soccerMatchScoreStore,
|
||||
config.nflGameScoreStore,
|
||||
config.semanticCoreMegadataStore
|
||||
)
|
||||
case CRT.MagicFanoutProductLaunch =>
|
||||
MagicFanoutProductLaunchCandidateGenerator.getCandidate
|
||||
case CRT.NewCreator =>
|
||||
MagicFanoutCreatorEventCandidateGenerator.getCandidate
|
||||
case CRT.CreatorSubscriber =>
|
||||
MagicFanoutCreatorEventCandidateGenerator.getCandidate
|
||||
case _ =>
|
||||
throw new UnsupportedCrtException(
|
||||
"UnsupportedCrtException for SendHandler: " + frigateNotification.commonRecommendationType)
|
||||
}
|
||||
}
|
||||
|
||||
candidateGenerator(target, frigateNotification)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,55 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.common.base.ScheduledSpaceSpeakerCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.util.Future
|
||||
|
||||
object ScheduledSpaceSpeakerCandidateGenerator extends CandidateGenerator {
|
||||
|
||||
override def getCandidate(
|
||||
targetUser: Target,
|
||||
notification: FrigateNotification
|
||||
): Future[RawCandidate] = {
|
||||
|
||||
/**
|
||||
* frigateNotification recommendation type should be [[CommonRecommendationType.ScheduledSpaceSpeaker]]
|
||||
*
|
||||
**/
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.ScheduledSpaceSpeaker,
|
||||
"ScheduledSpaceSpeaker: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
|
||||
val spaceNotification = notification.spaceNotification.getOrElse(
|
||||
throw new IllegalStateException("ScheduledSpaceSpeaker notification object not defined"))
|
||||
|
||||
require(
|
||||
spaceNotification.hostUserId.isDefined,
|
||||
"ScheduledSpaceSpeaker notification - hostUserId not defined"
|
||||
)
|
||||
|
||||
val spaceHostId = spaceNotification.hostUserId
|
||||
|
||||
require(
|
||||
spaceNotification.scheduledStartTime.isDefined,
|
||||
"ScheduledSpaceSpeaker notification - scheduledStartTime not defined"
|
||||
)
|
||||
|
||||
val scheduledStartTime = spaceNotification.scheduledStartTime.get
|
||||
|
||||
val candidate = new RawCandidate with ScheduledSpaceSpeakerCandidate {
|
||||
override val target: Target = targetUser
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
override val spaceId: String = spaceNotification.broadcastId
|
||||
override val hostId: Option[Long] = spaceHostId
|
||||
override val startTime: Long = scheduledStartTime
|
||||
override val speakerIds: Option[Seq[Long]] = spaceNotification.speakers
|
||||
override val listenerIds: Option[Seq[Long]] = spaceNotification.listeners
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,55 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.send_handler.generator
|
||||
|
||||
import com.twitter.frigate.common.base.ScheduledSpaceSubscriberCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.util.Future
|
||||
|
||||
object ScheduledSpaceSubscriberCandidateGenerator extends CandidateGenerator {
|
||||
|
||||
override def getCandidate(
|
||||
targetUser: Target,
|
||||
notification: FrigateNotification
|
||||
): Future[RawCandidate] = {
|
||||
|
||||
/**
|
||||
* frigateNotification recommendation type should be [[CommonRecommendationType.ScheduledSpaceSubscriber]]
|
||||
*
|
||||
**/
|
||||
require(
|
||||
notification.commonRecommendationType == CommonRecommendationType.ScheduledSpaceSubscriber,
|
||||
"ScheduledSpaceSubscriber: unexpected CRT " + notification.commonRecommendationType
|
||||
)
|
||||
|
||||
val spaceNotification = notification.spaceNotification.getOrElse(
|
||||
throw new IllegalStateException("ScheduledSpaceSubscriber notification object not defined"))
|
||||
|
||||
require(
|
||||
spaceNotification.hostUserId.isDefined,
|
||||
"ScheduledSpaceSubscriber notification - hostUserId not defined"
|
||||
)
|
||||
|
||||
val spaceHostId = spaceNotification.hostUserId
|
||||
|
||||
require(
|
||||
spaceNotification.scheduledStartTime.isDefined,
|
||||
"ScheduledSpaceSubscriber notification - scheduledStartTime not defined"
|
||||
)
|
||||
|
||||
val scheduledStartTime = spaceNotification.scheduledStartTime.get
|
||||
|
||||
val candidate = new RawCandidate with ScheduledSpaceSubscriberCandidate {
|
||||
override val target: Target = targetUser
|
||||
override val frigateNotification: FrigateNotification = notification
|
||||
override val spaceId: String = spaceNotification.broadcastId
|
||||
override val hostId: Option[Long] = spaceHostId
|
||||
override val startTime: Long = scheduledStartTime
|
||||
override val speakerIds: Option[Seq[Long]] = spaceNotification.speakers
|
||||
override val listenerIds: Option[Seq[Long]] = spaceNotification.listeners
|
||||
}
|
||||
|
||||
Future.value(candidate)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,17 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.content_mixer.thriftscala.ContentMixer
|
||||
import com.twitter.content_mixer.thriftscala.ContentMixerRequest
|
||||
import com.twitter.content_mixer.thriftscala.ContentMixerResponse
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class ContentMixerStore(contentMixer: ContentMixer.MethodPerEndpoint)
|
||||
extends ReadableStore[ContentMixerRequest, ContentMixerResponse] {
|
||||
|
||||
override def get(request: ContentMixerRequest): Future[Option[ContentMixerResponse]] = {
|
||||
contentMixer.getCandidates(request).map { response =>
|
||||
Some(response)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,15 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.copyselectionservice.thriftscala._
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
class CopySelectionServiceStore(copySelectionServiceClient: CopySelectionService.FinagledClient)
|
||||
extends ReadableStore[CopySelectionRequestV1, Copy] {
|
||||
override def get(k: CopySelectionRequestV1): Future[Option[Copy]] =
|
||||
copySelectionServiceClient.getSelectedCopy(CopySelectionRequest.V1(k)).map {
|
||||
case CopySelectionResponse.V1(response) =>
|
||||
Some(response.selectedCopy)
|
||||
case _ => throw CopyServiceException(CopyServiceErrorCode.VersionNotFound)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,58 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.cr_mixer.thriftscala.CrMixer
|
||||
import com.twitter.cr_mixer.thriftscala.CrMixerTweetRequest
|
||||
import com.twitter.cr_mixer.thriftscala.CrMixerTweetResponse
|
||||
import com.twitter.cr_mixer.thriftscala.FrsTweetRequest
|
||||
import com.twitter.cr_mixer.thriftscala.FrsTweetResponse
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.Stat
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.util.Future
|
||||
|
||||
/**
|
||||
* Store to get content recs from content recommender.
|
||||
*/
|
||||
case class CrMixerTweetStore(
|
||||
crMixer: CrMixer.MethodPerEndpoint
|
||||
)(
|
||||
implicit statsReceiver: StatsReceiver = NullStatsReceiver) {
|
||||
|
||||
private val requestsCounter = statsReceiver.counter("requests")
|
||||
private val successCounter = statsReceiver.counter("success")
|
||||
private val failuresCounter = statsReceiver.counter("failures")
|
||||
private val nonEmptyCounter = statsReceiver.counter("non_empty")
|
||||
private val emptyCounter = statsReceiver.counter("empty")
|
||||
private val failuresScope = statsReceiver.scope("failures")
|
||||
private val latencyStat = statsReceiver.stat("latency")
|
||||
|
||||
private def updateStats[T](f: => Future[Option[T]]): Future[Option[T]] = {
|
||||
requestsCounter.incr()
|
||||
Stat
|
||||
.timeFuture(latencyStat)(f)
|
||||
.onSuccess { r =>
|
||||
if (r.isDefined) nonEmptyCounter.incr() else emptyCounter.incr()
|
||||
successCounter.incr()
|
||||
}
|
||||
.onFailure { e =>
|
||||
{
|
||||
failuresCounter.incr()
|
||||
failuresScope.counter(e.getClass.getName).incr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getTweetRecommendations(
|
||||
request: CrMixerTweetRequest
|
||||
): Future[Option[CrMixerTweetResponse]] = {
|
||||
updateStats(crMixer.getTweetRecommendations(request).map { response =>
|
||||
Some(response)
|
||||
})
|
||||
}
|
||||
|
||||
def getFRSTweetCandidates(request: FrsTweetRequest): Future[Option[FrsTweetResponse]] = {
|
||||
updateStats(crMixer.getFrsBasedTweetRecommendations(request).map { response =>
|
||||
Some(response)
|
||||
})
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,28 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.explore_ranker.thriftscala.ExploreRanker
|
||||
import com.twitter.explore_ranker.thriftscala.ExploreRankerResponse
|
||||
import com.twitter.explore_ranker.thriftscala.ExploreRankerRequest
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
/** A Store for Video Tweet Recommendations from Explore
|
||||
*
|
||||
* @param exploreRankerService
|
||||
*/
|
||||
case class ExploreRankerStore(exploreRankerService: ExploreRanker.MethodPerEndpoint)
|
||||
extends ReadableStore[ExploreRankerRequest, ExploreRankerResponse] {
|
||||
|
||||
/** Method to get video recommendations
|
||||
*
|
||||
* @param request explore ranker request object
|
||||
* @return
|
||||
*/
|
||||
override def get(
|
||||
request: ExploreRankerRequest
|
||||
): Future[Option[ExploreRankerResponse]] = {
|
||||
exploreRankerService.getRankedResults(request).map { response =>
|
||||
Some(response)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,46 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService
|
||||
import com.twitter.follow_recommendations.thriftscala.Recommendation
|
||||
import com.twitter.follow_recommendations.thriftscala.RecommendationRequest
|
||||
import com.twitter.follow_recommendations.thriftscala.RecommendationResponse
|
||||
import com.twitter.follow_recommendations.thriftscala.UserRecommendation
|
||||
import com.twitter.inject.Logging
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class FollowRecommendationsStore(
|
||||
frsClient: FollowRecommendationsThriftService.MethodPerEndpoint,
|
||||
statsReceiver: StatsReceiver)
|
||||
extends ReadableStore[RecommendationRequest, RecommendationResponse]
|
||||
with Logging {
|
||||
|
||||
private val scopedStats = statsReceiver.scope(getClass.getSimpleName)
|
||||
private val requests = scopedStats.counter("requests")
|
||||
private val valid = scopedStats.counter("valid")
|
||||
private val invalid = scopedStats.counter("invalid")
|
||||
private val numTotalResults = scopedStats.stat("total_results")
|
||||
private val numValidResults = scopedStats.stat("valid_results")
|
||||
|
||||
override def get(request: RecommendationRequest): Future[Option[RecommendationResponse]] = {
|
||||
requests.incr()
|
||||
frsClient.getRecommendations(request).map { response =>
|
||||
numTotalResults.add(response.recommendations.size)
|
||||
val validRecs = response.recommendations.filter {
|
||||
case Recommendation.User(_: UserRecommendation) =>
|
||||
valid.incr()
|
||||
true
|
||||
case _ =>
|
||||
invalid.incr()
|
||||
false
|
||||
}
|
||||
|
||||
numValidResults.add(validRecs.size)
|
||||
Some(
|
||||
RecommendationResponse(
|
||||
recommendations = validRecs
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,190 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.finagle.stats.BroadcastStatsReceiver
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.logger.MRLogger
|
||||
import com.twitter.frigate.common.store
|
||||
import com.twitter.frigate.common.store.Fail
|
||||
import com.twitter.frigate.common.store.IbisRequestInfo
|
||||
import com.twitter.frigate.common.store.IbisResponse
|
||||
import com.twitter.frigate.common.store.Sent
|
||||
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||
import com.twitter.ibis2.service.thriftscala.Flags
|
||||
import com.twitter.ibis2.service.thriftscala.FlowControl
|
||||
import com.twitter.ibis2.service.thriftscala.Ibis2Request
|
||||
import com.twitter.ibis2.service.thriftscala.Ibis2Response
|
||||
import com.twitter.ibis2.service.thriftscala.Ibis2ResponseStatus
|
||||
import com.twitter.ibis2.service.thriftscala.Ibis2Service
|
||||
import com.twitter.ibis2.service.thriftscala.NotificationNotSentCode
|
||||
import com.twitter.ibis2.service.thriftscala.TargetFanoutResult.NotSentReason
|
||||
import com.twitter.util.Future
|
||||
|
||||
trait Ibis2Store extends store.Ibis2Store {
|
||||
def send(ibis2Request: Ibis2Request, candidate: PushCandidate): Future[IbisResponse]
|
||||
}
|
||||
|
||||
case class PushIbis2Store(
|
||||
ibisClient: Ibis2Service.MethodPerEndpoint
|
||||
)(
|
||||
implicit val statsReceiver: StatsReceiver = NullStatsReceiver)
|
||||
extends Ibis2Store {
|
||||
private val log = MRLogger(this.getClass.getSimpleName)
|
||||
private val stats = statsReceiver.scope("ibis_v2_store")
|
||||
private val statsByCrt = stats.scope("byCrt")
|
||||
private val requestsByCrt = statsByCrt.scope("requests")
|
||||
private val failuresByCrt = statsByCrt.scope("failures")
|
||||
private val successByCrt = statsByCrt.scope("success")
|
||||
|
||||
private val statsByIbisModel = stats.scope("byIbisModel")
|
||||
private val requestsByIbisModel = statsByIbisModel.scope("requests")
|
||||
private val failuresByIbisModel = statsByIbisModel.scope("failures")
|
||||
private val successByIbisModel = statsByIbisModel.scope("success")
|
||||
|
||||
private[this] def ibisSend(
|
||||
ibis2Request: Ibis2Request,
|
||||
commonRecommendationType: CommonRecommendationType
|
||||
): Future[IbisResponse] = {
|
||||
val ibisModel = ibis2Request.modelName
|
||||
|
||||
val bStats = if (ibis2Request.flags.getOrElse(Flags()).darkWrite.contains(true)) {
|
||||
BroadcastStatsReceiver(
|
||||
Seq(
|
||||
stats,
|
||||
stats.scope("dark_write")
|
||||
)
|
||||
)
|
||||
} else BroadcastStatsReceiver(Seq(stats))
|
||||
|
||||
bStats.counter("requests").incr()
|
||||
requestsByCrt.counter(commonRecommendationType.name).incr()
|
||||
requestsByIbisModel.counter(ibisModel).incr()
|
||||
|
||||
retry(ibisClient, ibis2Request, 3, bStats)
|
||||
.map { response =>
|
||||
bStats.counter(response.status.status.name).incr()
|
||||
successByCrt.counter(response.status.status.name, commonRecommendationType.name).incr()
|
||||
successByIbisModel.counter(response.status.status.name, ibisModel).incr()
|
||||
response.status.status match {
|
||||
case Ibis2ResponseStatus.SuccessWithDeliveries |
|
||||
Ibis2ResponseStatus.SuccessNoDeliveries =>
|
||||
IbisResponse(Sent, Some(response))
|
||||
case _ =>
|
||||
IbisResponse(Fail, Some(response))
|
||||
}
|
||||
}
|
||||
.onFailure { ex =>
|
||||
bStats.counter("failures").incr()
|
||||
val exceptionName = ex.getClass.getCanonicalName
|
||||
bStats.scope("failures").counter(exceptionName).incr()
|
||||
failuresByCrt.counter(exceptionName, commonRecommendationType.name).incr()
|
||||
failuresByIbisModel.counter(exceptionName, ibisModel).incr()
|
||||
}
|
||||
}
|
||||
|
||||
private def getNotifNotSentReason(
|
||||
ibis2Response: Ibis2Response
|
||||
): Option[NotificationNotSentCode] = {
|
||||
ibis2Response.status.fanoutResults match {
|
||||
case Some(fanoutResult) =>
|
||||
fanoutResult.pushResult.flatMap { pushResult =>
|
||||
pushResult.results.headOption match {
|
||||
case Some(NotSentReason(notSentInfo)) => Some(notSentInfo.notSentCode)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def send(ibis2Request: Ibis2Request, candidate: PushCandidate): Future[IbisResponse] = {
|
||||
val requestWithIID = if (ibis2Request.flowControl.exists(_.externalIid.isDefined)) {
|
||||
ibis2Request
|
||||
} else {
|
||||
ibis2Request.copy(
|
||||
flowControl = Some(
|
||||
ibis2Request.flowControl
|
||||
.getOrElse(FlowControl())
|
||||
.copy(externalIid = Some(candidate.impressionId))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val commonRecommendationType = candidate.frigateNotification.commonRecommendationType
|
||||
|
||||
ibisSend(requestWithIID, commonRecommendationType)
|
||||
.onSuccess { response =>
|
||||
response.ibis2Response.foreach { ibis2Response =>
|
||||
getNotifNotSentReason(ibis2Response).foreach { notifNotSentCode =>
|
||||
stats.scope(ibis2Response.status.status.name).counter(s"$notifNotSentCode").incr()
|
||||
}
|
||||
if (ibis2Response.status.status != Ibis2ResponseStatus.SuccessWithDeliveries) {
|
||||
log.warning(
|
||||
s"Request dropped on ibis for ${ibis2Request.recipientSelector.recipientId}: $ibis2Response")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure { ex =>
|
||||
log.warning(
|
||||
s"Ibis Request failure: ${ex.getClass.getCanonicalName} \n For IbisRequest: $ibis2Request")
|
||||
log.error(ex, ex.getMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// retry request when Ibis2ResponseStatus is PreFanoutError
|
||||
def retry(
|
||||
ibisClient: Ibis2Service.MethodPerEndpoint,
|
||||
request: Ibis2Request,
|
||||
retryCount: Int,
|
||||
bStats: StatsReceiver
|
||||
): Future[Ibis2Response] = {
|
||||
ibisClient.sendNotification(request).flatMap { response =>
|
||||
response.status.status match {
|
||||
case Ibis2ResponseStatus.PreFanoutError if retryCount > 0 =>
|
||||
bStats.scope("requests").counter("retry").incr()
|
||||
bStats.counter(response.status.status.name).incr()
|
||||
retry(ibisClient, request, retryCount - 1, bStats)
|
||||
case _ =>
|
||||
Future.value(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def send(
|
||||
ibis2Request: Ibis2Request,
|
||||
requestInfo: IbisRequestInfo
|
||||
): Future[IbisResponse] = {
|
||||
ibisSend(ibis2Request, requestInfo.commonRecommendationType)
|
||||
}
|
||||
}
|
||||
|
||||
case class StagingIbis2Store(remoteIbis2Store: PushIbis2Store) extends Ibis2Store {
|
||||
|
||||
final def addDarkWriteFlagIbis2Request(
|
||||
isTeamMember: Boolean,
|
||||
ibis2Request: Ibis2Request
|
||||
): Ibis2Request = {
|
||||
val flags =
|
||||
ibis2Request.flags.getOrElse(Flags())
|
||||
val darkWrite: Boolean = !isTeamMember || flags.darkWrite.getOrElse(false)
|
||||
ibis2Request.copy(flags = Some(flags.copy(darkWrite = Some(darkWrite))))
|
||||
}
|
||||
|
||||
override def send(ibis2Request: Ibis2Request, candidate: PushCandidate): Future[IbisResponse] = {
|
||||
candidate.target.isTeamMember.flatMap { isTeamMember =>
|
||||
val ibis2Req = addDarkWriteFlagIbis2Request(isTeamMember, ibis2Request)
|
||||
remoteIbis2Store.send(ibis2Req, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
override def send(
|
||||
ibis2Request: Ibis2Request,
|
||||
requestInfo: IbisRequestInfo
|
||||
): Future[IbisResponse] = {
|
||||
requestInfo.isTeamMember.flatMap { isTeamMember =>
|
||||
val ibis2Req = addDarkWriteFlagIbis2Request(isTeamMember, ibis2Request)
|
||||
remoteIbis2Store.send(ibis2Req, requestInfo)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,16 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.interests_discovery.thriftscala.InterestsDiscoveryService
|
||||
import com.twitter.interests_discovery.thriftscala.RecommendedListsRequest
|
||||
import com.twitter.interests_discovery.thriftscala.RecommendedListsResponse
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class InterestDiscoveryStore(
|
||||
client: InterestsDiscoveryService.MethodPerEndpoint)
|
||||
extends ReadableStore[RecommendedListsRequest, RecommendedListsResponse] {
|
||||
|
||||
override def get(request: RecommendedListsRequest): Future[Option[RecommendedListsResponse]] = {
|
||||
client.getListRecos(request).map(Some(_))
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,156 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.conversions.DurationOps._
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.frigate.common.candidate.TargetDecider
|
||||
import com.twitter.frigate.common.history.History
|
||||
import com.twitter.frigate.common.history.HistoryStoreKeyContext
|
||||
import com.twitter.frigate.common.history.PushServiceHistoryStore
|
||||
import com.twitter.frigate.data_pipeline.thriftscala._
|
||||
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||
import com.twitter.hermit.store.labeled_push_recs.LabeledPushRecsJoinedWithNotificationHistoryStore
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.util.Time
|
||||
|
||||
case class LabeledPushRecsVerifyingStoreKey(
|
||||
historyStoreKey: HistoryStoreKeyContext,
|
||||
useHydratedDataset: Boolean,
|
||||
verifyHydratedDatasetResults: Boolean) {
|
||||
def userId: Long = historyStoreKey.targetUserId
|
||||
}
|
||||
|
||||
case class LabeledPushRecsVerifyingStoreResponse(
|
||||
userHistory: UserHistoryValue,
|
||||
unequalNotificationsUnhydratedToHydrated: Option[
|
||||
Map[(Time, FrigateNotification), FrigateNotification]
|
||||
],
|
||||
missingFromHydrated: Option[Map[Time, FrigateNotification]])
|
||||
|
||||
case class LabeledPushRecsVerifyingStore(
|
||||
labeledPushRecsStore: ReadableStore[UserHistoryKey, UserHistoryValue],
|
||||
historyStore: PushServiceHistoryStore
|
||||
)(
|
||||
implicit stats: StatsReceiver)
|
||||
extends ReadableStore[LabeledPushRecsVerifyingStoreKey, LabeledPushRecsVerifyingStoreResponse] {
|
||||
|
||||
private def getByJoiningWithRealHistory(
|
||||
key: HistoryStoreKeyContext
|
||||
): Future[Option[UserHistoryValue]] = {
|
||||
val historyFut = historyStore.get(key, Some(365.days))
|
||||
val toJoinWithRealHistoryFut = labeledPushRecsStore.get(UserHistoryKey.UserId(key.targetUserId))
|
||||
Future.join(historyFut, toJoinWithRealHistoryFut).map {
|
||||
case (_, None) => None
|
||||
case (History(realtimeHistoryMap), Some(uhValue)) =>
|
||||
Some(
|
||||
LabeledPushRecsJoinedWithNotificationHistoryStore
|
||||
.joinLabeledPushRecsSentWithNotificationHistory(uhValue, realtimeHistoryMap, stats)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def processUserHistoryValue(uhValue: UserHistoryValue): Map[Time, FrigateNotification] = {
|
||||
uhValue.events
|
||||
.getOrElse(Nil)
|
||||
.collect {
|
||||
case Event(
|
||||
EventType.LabeledPushRecSend,
|
||||
Some(tsMillis),
|
||||
Some(EventUnion.LabeledPushRecSendEvent(lprs: LabeledPushRecSendEvent))
|
||||
) if lprs.pushRecSendEvent.frigateNotification.isDefined =>
|
||||
Time.fromMilliseconds(tsMillis) -> lprs.pushRecSendEvent.frigateNotification.get
|
||||
}
|
||||
.toMap
|
||||
}
|
||||
|
||||
override def get(
|
||||
key: LabeledPushRecsVerifyingStoreKey
|
||||
): Future[Option[LabeledPushRecsVerifyingStoreResponse]] = {
|
||||
val uhKey = UserHistoryKey.UserId(key.userId)
|
||||
if (!key.useHydratedDataset) {
|
||||
getByJoiningWithRealHistory(key.historyStoreKey).map { uhValueOpt =>
|
||||
uhValueOpt.map { uhValue => LabeledPushRecsVerifyingStoreResponse(uhValue, None, None) }
|
||||
}
|
||||
} else {
|
||||
labeledPushRecsStore.get(uhKey).flatMap { hydratedValueOpt: Option[UserHistoryValue] =>
|
||||
if (!key.verifyHydratedDatasetResults) {
|
||||
Future.value(hydratedValueOpt.map { uhValue =>
|
||||
LabeledPushRecsVerifyingStoreResponse(uhValue, None, None)
|
||||
})
|
||||
} else {
|
||||
getByJoiningWithRealHistory(key.historyStoreKey).map {
|
||||
joinedWithRealHistoryOpt: Option[UserHistoryValue] =>
|
||||
val joinedWithRealHistoryMap =
|
||||
joinedWithRealHistoryOpt.map(processUserHistoryValue).getOrElse(Map.empty)
|
||||
val hydratedMap = hydratedValueOpt.map(processUserHistoryValue).getOrElse(Map.empty)
|
||||
val unequal = joinedWithRealHistoryMap.flatMap {
|
||||
case (time, frigateNotif) =>
|
||||
hydratedMap.get(time).collect {
|
||||
case n if n != frigateNotif => ((time, frigateNotif), n)
|
||||
}
|
||||
}
|
||||
val missing = joinedWithRealHistoryMap.filter {
|
||||
case (time, frigateNotif) => !hydratedMap.contains(time)
|
||||
}
|
||||
hydratedValueOpt.map { hydratedValue =>
|
||||
LabeledPushRecsVerifyingStoreResponse(hydratedValue, Some(unequal), Some(missing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class LabeledPushRecsStoreKey(target: TargetDecider, historyStoreKey: HistoryStoreKeyContext) {
|
||||
def userId: Long = historyStoreKey.targetUserId
|
||||
}
|
||||
|
||||
case class LabeledPushRecsDecideredStore(
|
||||
verifyingStore: ReadableStore[
|
||||
LabeledPushRecsVerifyingStoreKey,
|
||||
LabeledPushRecsVerifyingStoreResponse
|
||||
],
|
||||
useHydratedLabeledSendsDatasetDeciderKey: String,
|
||||
verifyHydratedLabeledSendsForHistoryDeciderKey: String
|
||||
)(
|
||||
implicit globalStats: StatsReceiver)
|
||||
extends ReadableStore[LabeledPushRecsStoreKey, UserHistoryValue] {
|
||||
private val log = Logger()
|
||||
private val stats = globalStats.scope("LabeledPushRecsDecideredStore")
|
||||
private val numComparisons = stats.counter("num_comparisons")
|
||||
private val numMissingStat = stats.stat("num_missing")
|
||||
private val numUnequalStat = stats.stat("num_unequal")
|
||||
|
||||
override def get(key: LabeledPushRecsStoreKey): Future[Option[UserHistoryValue]] = {
|
||||
val useHydrated = key.target.isDeciderEnabled(
|
||||
useHydratedLabeledSendsDatasetDeciderKey,
|
||||
stats,
|
||||
useRandomRecipient = true
|
||||
)
|
||||
|
||||
val verifyHydrated = if (useHydrated) {
|
||||
key.target.isDeciderEnabled(
|
||||
verifyHydratedLabeledSendsForHistoryDeciderKey,
|
||||
stats,
|
||||
useRandomRecipient = true
|
||||
)
|
||||
} else false
|
||||
|
||||
val newKey = LabeledPushRecsVerifyingStoreKey(key.historyStoreKey, useHydrated, verifyHydrated)
|
||||
verifyingStore.get(newKey).map {
|
||||
case None => None
|
||||
case Some(LabeledPushRecsVerifyingStoreResponse(uhValue, unequalOpt, missingOpt)) =>
|
||||
(unequalOpt, missingOpt) match {
|
||||
case (Some(unequal), Some(missing)) =>
|
||||
numComparisons.incr()
|
||||
numMissingStat.add(missing.size)
|
||||
numUnequalStat.add(unequal.size)
|
||||
case _ => //no-op
|
||||
}
|
||||
Some(uhValue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
@ -1,26 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.livevideo.common.ids.EventId
|
||||
import com.twitter.livevideo.timeline.client.v2.LiveVideoTimelineClient
|
||||
import com.twitter.livevideo.timeline.domain.v2.Event
|
||||
import com.twitter.livevideo.timeline.domain.v2.LookupContext
|
||||
import com.twitter.stitch.storehaus.ReadableStoreOfStitch
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
|
||||
case class EventRequest(eventId: Long, lookupContext: LookupContext = LookupContext.default)
|
||||
|
||||
object LexServiceStore {
|
||||
def apply(
|
||||
liveVideoTimelineClient: LiveVideoTimelineClient
|
||||
): ReadableStore[EventRequest, Event] = {
|
||||
ReadableStoreOfStitch { eventRequest =>
|
||||
liveVideoTimelineClient.getEvent(
|
||||
EventId(eventRequest.eventId),
|
||||
eventRequest.lookupContext) rescue {
|
||||
case NotFound => Stitch.NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,45 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.hermit.store.common.ReadableWritableStore
|
||||
import com.twitter.notificationservice.thriftscala.GenericNotificationOverrideKey
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.BinaryCompactScalaInjection
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.LongInjection
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.StringInjection
|
||||
import com.twitter.storage.client.manhattan.kv.ManhattanKVEndpoint
|
||||
import com.twitter.storage.client.manhattan.kv.impl.Component
|
||||
import com.twitter.storage.client.manhattan.kv.impl.DescriptorP1L1
|
||||
import com.twitter.storage.client.manhattan.kv.impl.KeyDescriptor
|
||||
import com.twitter.storage.client.manhattan.kv.impl.ValueDescriptor
|
||||
import com.twitter.util.Future
|
||||
|
||||
case class NTabHistoryStore(mhEndpoint: ManhattanKVEndpoint, dataset: String)
|
||||
extends ReadableWritableStore[(Long, String), GenericNotificationOverrideKey] {
|
||||
|
||||
private val keyDesc: DescriptorP1L1.EmptyKey[Long, String] =
|
||||
KeyDescriptor(Component(LongInjection), Component(StringInjection))
|
||||
|
||||
private val genericNotifKeyValDesc: ValueDescriptor.EmptyValue[GenericNotificationOverrideKey] =
|
||||
ValueDescriptor[GenericNotificationOverrideKey](
|
||||
BinaryCompactScalaInjection(GenericNotificationOverrideKey)
|
||||
)
|
||||
|
||||
override def get(key: (Long, String)): Future[Option[GenericNotificationOverrideKey]] = {
|
||||
val (userId, impressionId) = key
|
||||
val mhKey = keyDesc.withDataset(dataset).withPkey(userId).withLkey(impressionId)
|
||||
|
||||
Stitch
|
||||
.run(mhEndpoint.get(mhKey, genericNotifKeyValDesc))
|
||||
.map { optionMhValue =>
|
||||
optionMhValue.map(_.contents)
|
||||
}
|
||||
}
|
||||
|
||||
override def put(keyValue: ((Long, String), GenericNotificationOverrideKey)): Future[Unit] = {
|
||||
val ((userId, impressionId), genericNotifOverrideKey) = keyValue
|
||||
val mhKey = keyDesc.withDataset(dataset).withPkey(userId).withLkey(impressionId)
|
||||
val mhVal = genericNotifKeyValDesc.withValue(genericNotifOverrideKey)
|
||||
Stitch.run(mhEndpoint.insert(mhKey, mhVal))
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
@ -1,73 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.conversions.DurationOps._
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.onboarding.task.service.thriftscala.FatigueFlowEnrollment
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.BinaryScalaInjection
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.LongInjection
|
||||
import com.twitter.storage.client.manhattan.bijections.Bijections.StringInjection
|
||||
import com.twitter.storage.client.manhattan.kv.impl.Component
|
||||
import com.twitter.storage.client.manhattan.kv.impl.KeyDescriptor
|
||||
import com.twitter.storage.client.manhattan.kv.impl.ValueDescriptor
|
||||
import com.twitter.storage.client.manhattan.kv.ManhattanKVClient
|
||||
import com.twitter.storage.client.manhattan.kv.ManhattanKVClientMtlsParams
|
||||
import com.twitter.storage.client.manhattan.kv.ManhattanKVEndpointBuilder
|
||||
import com.twitter.storage.client.manhattan.kv.NoMtlsParams
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.storehaus_internal.manhattan.Omega
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.util.Time
|
||||
|
||||
case class OCFHistoryStoreKey(userId: Long, fatigueDuration: Duration, fatigueGroup: String)
|
||||
|
||||
class OCFPromptHistoryStore(
|
||||
manhattanAppId: String,
|
||||
dataset: String,
|
||||
mtlsParams: ManhattanKVClientMtlsParams = NoMtlsParams
|
||||
)(
|
||||
implicit stats: StatsReceiver)
|
||||
extends ReadableStore[OCFHistoryStoreKey, FatigueFlowEnrollment] {
|
||||
|
||||
import ManhattanInjections._
|
||||
|
||||
private val client = ManhattanKVClient(
|
||||
appId = manhattanAppId,
|
||||
dest = Omega.wilyName,
|
||||
mtlsParams = mtlsParams,
|
||||
label = "ocf_history_store"
|
||||
)
|
||||
private val endpoint = ManhattanKVEndpointBuilder(client, defaultMaxTimeout = 5.seconds)
|
||||
.statsReceiver(stats.scope("ocf_history_store"))
|
||||
.build()
|
||||
|
||||
private val limitResultsTo = 1
|
||||
|
||||
private val datasetKey = keyDesc.withDataset(dataset)
|
||||
|
||||
override def get(storeKey: OCFHistoryStoreKey): Future[Option[FatigueFlowEnrollment]] = {
|
||||
val userId = storeKey.userId
|
||||
val fatigueGroup = storeKey.fatigueGroup
|
||||
val fatigueLength = storeKey.fatigueDuration.inMilliseconds
|
||||
val currentTime = Time.now.inMilliseconds
|
||||
val fullKey = datasetKey
|
||||
.withPkey(userId)
|
||||
.from(fatigueGroup)
|
||||
.to(fatigueGroup, fatigueLength - currentTime)
|
||||
|
||||
Stitch
|
||||
.run(endpoint.slice(fullKey, valDesc, limit = Some(limitResultsTo)))
|
||||
.map { results =>
|
||||
if (results.nonEmpty) {
|
||||
val (_, mhValue) = results.head
|
||||
Some(mhValue.contents)
|
||||
} else None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ManhattanInjections {
|
||||
val keyDesc = KeyDescriptor(Component(LongInjection), Component(StringInjection, LongInjection))
|
||||
val valDesc = ValueDescriptor(BinaryScalaInjection(FatigueFlowEnrollment))
|
||||
}
|
Binary file not shown.
@ -1,81 +0,0 @@
|
||||
package com.twitter.frigate.pushservice.store
|
||||
|
||||
import com.twitter.conversions.DurationOps._
|
||||
import com.twitter.frigate.common.history.History
|
||||
import com.twitter.frigate.common.store.RealTimeClientEventStore
|
||||
import com.twitter.frigate.data_pipeline.common.HistoryJoin
|
||||
import com.twitter.frigate.data_pipeline.thriftscala.Event
|
||||
import com.twitter.frigate.data_pipeline.thriftscala.EventUnion
|
||||
import com.twitter.frigate.data_pipeline.thriftscala.PushRecSendEvent
|
||||
import com.twitter.frigate.data_pipeline.thriftscala.UserHistoryValue
|
||||
import com.twitter.storehaus.ReadableStore
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Future
|
||||
import com.twitter.util.Time
|
||||
|
||||
case class OnlineUserHistoryKey(
|
||||
userId: Long,
|
||||
offlineUserHistory: Option[UserHistoryValue],
|
||||
history: Option[History])
|
||||
|
||||
case class OnlineUserHistoryStore(
|
||||
realTimeClientEventStore: RealTimeClientEventStore,
|
||||
duration: Duration = 3.days)
|
||||
extends ReadableStore[OnlineUserHistoryKey, UserHistoryValue] {
|
||||
|
||||
override def get(key: OnlineUserHistoryKey): Future[Option[UserHistoryValue]] = {
|
||||
val now = Time.now
|
||||
|
||||
val pushRecSends = key.history
|
||||
.getOrElse(History(Nil.toMap))
|
||||
.sortedPushDmHistory
|
||||
.filter(_._1 > now - (duration + 1.day))
|
||||
.map {
|
||||
case (time, frigateNotification) =>
|
||||
val pushRecSendEvent = PushRecSendEvent(
|
||||
frigateNotification = Some(frigateNotification),
|
||||
impressionId = frigateNotification.impressionId
|
||||
)
|
||||
pushRecSendEvent -> time
|
||||
}
|
||||
|
||||
realTimeClientEventStore
|
||||
.get(key.userId, now - duration, now)
|
||||
.map { attributedEventHistory =>
|
||||
val attributedClientEvents = attributedEventHistory.sortedHistory.flatMap {
|
||||
case (time, event) =>
|
||||
event.eventUnion match {
|
||||
case Some(eventUnion: EventUnion.AttributedPushRecClientEvent) =>
|
||||
Some((eventUnion.attributedPushRecClientEvent, event.eventType, time))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
val realtimeLabeledSends: Seq[Event] = HistoryJoin.getLabeledPushRecSends(
|
||||
pushRecSends,
|
||||
attributedClientEvents,
|
||||
Seq(),
|
||||
Seq(),
|
||||
Seq(),
|
||||
now
|
||||
)
|
||||
|
||||
key.offlineUserHistory.map { offlineUserHistory =>
|
||||
val combinedEvents = offlineUserHistory.events.map { offlineEvents =>
|
||||
(offlineEvents ++ realtimeLabeledSends)
|
||||
.map { event =>
|
||||
event.timestampMillis -> event
|
||||
}
|
||||
.toMap
|
||||
.values
|
||||
.toSeq
|
||||
.sortBy { event =>
|
||||
-1 * event.timestampMillis.getOrElse(0L)
|
||||
}
|
||||
}
|
||||
|
||||
offlineUserHistory.copy(events = combinedEvents)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user