[docx] split commit for file 1200

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

View File

@ -1,27 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.modify_social_proof
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.base.GatedTransform
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext
import com.twitter.stitch.Stitch
import com.twitter.timelines.configapi.HasParams
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RemoveAccountProofTransform @Inject() (statsReceiver: StatsReceiver)
extends GatedTransform[HasClientContext with HasParams, CandidateUser] {
private val stats = statsReceiver.scope(this.getClass.getSimpleName)
private val removedProofsCounter = stats.counter("num_removed_proofs")
override def transform(
target: HasClientContext with HasParams,
items: Seq[CandidateUser]
): Stitch[Seq[CandidateUser]] =
Stitch.value(items.map { candidate =>
removedProofsCounter.incr()
candidate.copy(reason = None)
})
}

View File

@ -1,19 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/google/inject:guice",
"3rdparty/jvm/com/google/inject/extensions:guice-assistedinject",
"3rdparty/jvm/net/codingwell:scala-guice",
"3rdparty/jvm/org/slf4j:slf4j-api",
"finatra/inject/inject-core/src/main/scala",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils",
"hermit/hermit-core/src/main/scala/com/twitter/hermit/constants",
"util/util-slf4j-api/src/main/scala",
],
)

View File

@ -1,24 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.ranker_id
import com.google.inject.Inject
import com.google.inject.Singleton
import com.twitter.follow_recommendations.common.base.GatedTransform
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.follow_recommendations.common.models.Score
import com.twitter.stitch.Stitch
import com.twitter.timelines.configapi.HasParams
/**
* This class appends each candidate's rankerIds with the RandomRankerId.
* This is primarily for determining if a candidate was generated via random shuffling.
*/
@Singleton
class RandomRankerIdTransform @Inject() () extends GatedTransform[HasParams, CandidateUser] {
override def transform(
target: HasParams,
candidates: Seq[CandidateUser]
): Stitch[Seq[CandidateUser]] = {
Stitch.value(candidates.map(_.addScore(Score.RandomScore)))
}
}

View File

@ -1,20 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.recommendation_flow_identifier
import com.google.inject.Inject
import com.twitter.follow_recommendations.common.base.Transform
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.follow_recommendations.common.models.HasRecommendationFlowIdentifier
import com.twitter.stitch.Stitch
class AddRecommendationFlowIdentifierTransform @Inject()
extends Transform[HasRecommendationFlowIdentifier, CandidateUser] {
override def transform(
target: HasRecommendationFlowIdentifier,
items: Seq[CandidateUser]
): Stitch[Seq[CandidateUser]] = {
Stitch.value(items.map { candidateUser =>
candidateUser.copy(recommendationFlowIdentifier = target.recommendationFlowIdentifier)
})
}
}

View File

@ -1,9 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
],
)

View File

@ -1,18 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/google/inject:guice",
"3rdparty/jvm/com/google/inject/extensions:guice-assistedinject",
"3rdparty/jvm/net/codingwell:scala-guice",
"3rdparty/jvm/org/slf4j:slf4j-api",
"finatra/inject/inject-core/src/main/scala",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils",
"hermit/hermit-core/src/main/scala/com/twitter/hermit/constants",
"util/util-slf4j-api/src/main/scala",
],
)

View File

@ -1,76 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.tracking_token
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.base.Transform
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.follow_recommendations.common.models.HasDisplayLocation
import com.twitter.follow_recommendations.common.models.Session
import com.twitter.follow_recommendations.common.models.TrackingToken
import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap
import com.twitter.hermit.model.Algorithm
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext
import com.twitter.stitch.Stitch
import com.twitter.util.logging.Logging
import javax.inject.Inject
import javax.inject.Singleton
/**
* This transform adds the tracking token for all candidates
* Since this happens in the same request, we use the same trace-id for all candidates
* There are no RPC calls in this transform so it's safe to chain it with `andThen` at the end of
* all other product-specific transforms
*/
@Singleton
class TrackingTokenTransform @Inject() (baseStatsReceiver: StatsReceiver)
extends Transform[HasDisplayLocation with HasClientContext, CandidateUser]
with Logging {
def profileResults(
target: HasDisplayLocation with HasClientContext,
candidates: Seq[CandidateUser]
) = {
// Metrics to track # results per candidate source
val stats = baseStatsReceiver.scope(target.displayLocation.toString + "/final_results")
stats.stat("total").add(candidates.size)
stats.counter(target.displayLocation.toString).incr()
val flattenedCandidates: Seq[(CandidateSourceIdentifier, CandidateUser)] = for {
candidate <- candidates
identifier <- candidate.getPrimaryCandidateSource
} yield (identifier, candidate)
val candidatesGroupedBySource: Map[CandidateSourceIdentifier, Seq[CandidateUser]] =
flattenedCandidates.groupBy(_._1).mapValues(_.map(_._2))
candidatesGroupedBySource map {
case (source, candidates) => stats.stat(source.name).add(candidates.size)
}
}
override def transform(
target: HasDisplayLocation with HasClientContext,
candidates: Seq[CandidateUser]
): Stitch[Seq[CandidateUser]] = {
profileResults(target, candidates)
Stitch.value(
target.getOptionalUserId
.map { _ =>
candidates.map {
candidate =>
val token = Some(TrackingToken(
sessionId = Session.getSessionId,
displayLocation = Some(target.displayLocation),
controllerData = None,
algorithmId = candidate.userCandidateSourceDetails.flatMap(_.primaryCandidateSource
.flatMap { identifier =>
Algorithm.withNameOpt(identifier.name).flatMap(AlgorithmToFeedbackTokenMap.get)
})
))
candidate.copy(trackingToken = token)
}
}.getOrElse(candidates))
}
}

View File

@ -1,10 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common",
],
)

View File

@ -1,138 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.weighted_sampling
import com.twitter.follow_recommendations.common.base.GatedTransform
import com.twitter.stitch.Stitch
import com.twitter.timelines.configapi.HasParams
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.follow_recommendations.common.models.HasDebugOptions
import com.twitter.follow_recommendations.common.models.Score
import com.twitter.follow_recommendations.common.models.Scores
import com.twitter.follow_recommendations.common.rankers.common.RankerId
import com.twitter.follow_recommendations.common.rankers.utils.Utils
import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SamplingTransform @Inject() ()
extends GatedTransform[HasClientContext with HasParams with HasDebugOptions, CandidateUser] {
val name: String = this.getClass.getSimpleName
/*
Description: This function takes in a set of candidate users and ranks them for a who-to-follow
request by sampling from the Placket-Luce distribution
(https://cran.rstudio.com/web/packages/PlackettLuce/vignettes/Overview.html) with a three
variations. The first variation is that the scores of the candidates are multiplied by
multiplicativeFactor before sampling. The second variation is that the scores are
exponentiated before sampling. The third variation is that depending on how many who-to-follow
positions are being requested, the first k positions are reserved for the candidates with the
highest scores (and they are sorted in decreasing order of score) and the remaining positions
are sampled from a Placket-Luce. We use the efficient algorithm proposed in this blog
https://medium.com/swlh/going-old-school-designing-algorithms-for-fast-weighted-sampling-in-production-c48fc1f40051
to sample from a Plackett-Luce. Because of numerical stability reasons, before sampling from this
distribution, (1) we subtract off the maximum score from all the scores and (2) if after
this subtraction and multiplication by the multiplicative factor the resulting score is <= -10,
we force the candidate's transformed score under the above algorithm to be 0 (so r^(1/w) = 0)
where r is a random number and w is the transformed score.
inputs:
- target: HasClientContext (WTF request)
- candidates: sequence of CandidateUsers (users that need to be ranked from a who-to-follow
request) each of which has a score
inputs accessed through feature switches, i.e. through target.params (see the following file:
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/
transforms/weighted_sampling/SamplingTransformParams.scala"):
- topKFixed: the first k positions of the who-to-follow ranking correspond to the users with the k
highest scores and are not sampled from the Placket-Luce distribution
- multiplicativeFactor: multiplicativeFactor is used to transform the scores of each candidate by
multiplying that user's score by multiplicativeFactor
output:
- Sequence of CandidateUser whose order represents the ranking of users in a who-to-follow request
This ranking is sampled from a Placket-Luce distribution.
*/
override def transform(
target: HasClientContext with HasParams with HasDebugOptions,
candidates: Seq[CandidateUser]
): Stitch[Seq[CandidateUser]] = {
// the first k positions of the who-to-follow ranking correspond to the users with the k
// highest scores and are not sampled from the Placket-Luce distribution
val topKFixed = target.params(SamplingTransformParams.TopKFixed)
// multiplicativeFactor is used to transform the scores of each candidate by
// multiplying that user's score by multiplicativeFactor
val multiplicativeFactor = target.params(SamplingTransformParams.MultiplicativeFactor)
// sort candidates by their score
val candidatesSorted = candidates.sortBy(-1 * _.score.getOrElse(0.0))
// pick the top K candidates by score and the remaining candidates
val (topKFixedCandidates, candidatesOutsideOfTopK) =
candidatesSorted.zipWithIndex.partition { case (value, index) => index < topKFixed }
val randomNumGenerator =
new scala.util.Random(target.getRandomizationSeed.getOrElse(System.currentTimeMillis))
// we need to subtract the maximum score off the scores for numerical stability reasons
// subtracting the max score off does not effect the underlying distribution we are sampling
// the candidates from
// we need the if statement since you cannot take the max of an empty sequence
val maximum_score = if (candidatesOutsideOfTopK.nonEmpty) {
candidatesOutsideOfTopK.map(x => x._1.score.getOrElse(0.0)).max
} else {
0.0
}
// for candidates in candidatesOutsideOfTopK, we transform their score by subtracting off
// maximum_score and then multiply by multiplicativeFactor
val candidatesOutsideOfTopKTransformedScore = candidatesOutsideOfTopK.map(x =>
(x._1, multiplicativeFactor * (x._1.score.getOrElse(0.0) - maximum_score)))
// for each candidate with score transformed and clip score w, sample a random number r,
// create a new score r^(1/w) and sort the candidates to get the final ranking.
// for numerical stability reasons if the score is <=-10, we force r^(1/w) = 0.
// this samples the candidates from the modified Plackett-Luce distribution. See
// https://medium.com/swlh/going-old-school-designing-algorithms-for-fast-weighted-sampling-in-production-c48fc1f40051
val candidatesOutsideOfTopKSampled = candidatesOutsideOfTopKTransformedScore
.map(x =>
(
x._1,
if (x._2 <= -10.0)
0.0
else
scala.math.pow(
randomNumGenerator.nextFloat(),
1 / (scala.math
.exp(x._2))))).sortBy(-1 * _._2)
val topKCandidates: Seq[CandidateUser] = topKFixedCandidates.map(_._1)
val scribeRankingInfo: Boolean =
target.params(SamplingTransformParams.ScribeRankingInfoInSamplingTransform)
val transformedCandidates: Seq[CandidateUser] = if (scribeRankingInfo) {
val topKCandidatesWithRankingInfo: Seq[CandidateUser] =
Utils.addRankingInfo(topKCandidates, name)
val candidatesOutsideOfTopKSampledWithRankingInfo: Seq[CandidateUser] =
candidatesOutsideOfTopKSampled.zipWithIndex.map {
case ((candidate, score), rank) =>
val newScore = Seq(Score(score, Some(RankerId.PlacketLuceSamplingTransformer)))
val newScores: Option[Scores] = candidate.scores
.map { scores =>
scores.copy(scores = scores.scores ++ newScore)
}.orElse(Some(Scores(newScore, Some(RankerId.PlacketLuceSamplingTransformer))))
val globalRank = rank + topKFixed + 1
candidate.addInfoPerRankingStage(name, newScores, globalRank)
}
topKCandidatesWithRankingInfo ++ candidatesOutsideOfTopKSampledWithRankingInfo
} else {
topKCandidates ++ candidatesOutsideOfTopKSampled.map(_._1)
}
Stitch.value(transformedCandidates)
}
}

View File

@ -1,19 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.weighted_sampling
import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SamplingTransformFSConfig @Inject() () extends FeatureSwitchConfig {
override val intFSParams: Seq[FSBoundedParam[Int]] = Seq(SamplingTransformParams.TopKFixed)
override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq(
SamplingTransformParams.MultiplicativeFactor)
override val booleanFSParams: Seq[FSParam[Boolean]] = Seq(
SamplingTransformParams.ScribeRankingInfoInSamplingTransform)
}

View File

@ -1,25 +0,0 @@
package com.twitter.follow_recommendations.common.transforms.weighted_sampling
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
object SamplingTransformParams {
case object TopKFixed // indicates how many of the fisrt K who-to-follow recommendations are reserved for the candidates with largest K CandidateUser.score where these candidates are sorted in decreasing order of score
extends FSBoundedParam[Int](
name = "post_nux_ml_flow_weighted_sampling_top_k_fixed",
default = 0,
min = 0,
max = 100)
case object MultiplicativeFactor // CandidateUser.score gets transformed to multiplicativeFactor*CandidateUser.score before sampling from the Plackett-Luce distribution
extends FSBoundedParam[Double](
name = "post_nux_ml_flow_weighted_sampling_multiplicative_factor",
default = 1.0,
min = -1000.0,
max = 1000.0)
case object ScribeRankingInfoInSamplingTransform
extends FSParam[Boolean]("sampling_transform_scribe_ranking_info", false)
}

View File

@ -1,13 +0,0 @@
scala_library(
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"finatra/inject/inject-core/src/main/scala",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller/request",
"stitch/stitch-core",
"util/util-slf4j-api/src/main/scala",
],
)

View File

@ -1,22 +0,0 @@
package com.twitter.follow_recommendations.common.utils
object CollectionUtil {
/**
* Transposes a sequence of sequences. As opposed to the Scala collection library version
* of transpose, the sequences do not have to have the same length.
*
* Example:
* transpose(immutable.Seq(immutable.Seq(1,2,3), immutable.Seq(4,5), immutable.Seq(6,7)))
* => immutable.Seq(immutable.Seq(1, 4, 6), immutable.Seq(2, 5, 7), immutable.Seq(3))
*
* @param seq a sequence of sequences
* @tparam A the type of elements in the seq
* @return the transposed sequence of sequences
*/
def transposeLazy[A](seq: Seq[Seq[A]]): Stream[Seq[A]] =
seq.filter(_.nonEmpty) match {
case Nil => Stream.empty
case ys => ys.map(_.head) #:: transposeLazy(ys.map(_.tail))
}
}

View File

@ -1,27 +0,0 @@
package com.twitter.follow_recommendations.common.utils
import com.twitter.follow_recommendations.common.models.DisplayLocation
import com.twitter.follow_recommendations.common.models.Product
import com.twitter.product_mixer.core.model.marshalling.request.Product
object DisplayLocationProductConverterUtil {
def productToDisplayLocation(product: Product): DisplayLocation = {
product match {
case Product.MagicRecs => DisplayLocation.MagicRecs
case _ =>
throw UnconvertibleProductMixerProductException(
s"Cannot convert Product Mixer Product ${product.identifier.name} into a FRS DisplayLocation.")
}
}
def displayLocationToProduct(displayLocation: DisplayLocation): Product = {
displayLocation match {
case DisplayLocation.MagicRecs => Product.MagicRecs
case _ =>
throw UnconvertibleProductMixerProductException(
s"Cannot convert DisplayLocation ${displayLocation.toFsName} into a Product Mixer Product.")
}
}
}
case class UnconvertibleProductMixerProductException(message: String) extends Exception(message)

View File

@ -1,51 +0,0 @@
package com.twitter.follow_recommendations.common.utils
object MergeUtil {
/**
* Takes a seq of items which have weights. Returns an infinite stream of each item
* by their weights. All weights need to be greater than or equal to zero. In addition,
* the sum of weights should be greater than zero.
*
* Example usage of this function:
* Input weighted Item {{CS1, 3}, {CS2, 2}, {CS3, 5}}
* Output stream: (CS1, CS1, CS1, CS2, CS2, CS3, CS3, CS3, CS3, CS3, CS1, CS1, CS1, CS2,...}
*
* @param items items
* @param weighted provides weights for items
* @tparam T type of item
*
* @return Stream of Ts
*/
def weightedRoundRobin[T](
items: Seq[T]
)(
implicit weighted: Weighted[T]
): Stream[T] = {
if (items.isEmpty) {
Stream.empty
} else {
val weights = items.map { i => weighted(i) }
assert(
weights.forall {
_ >= 0
},
"Negative weight exists for sampling")
val cumulativeWeight = weights.scanLeft(0.0)(_ + _).tail
assert(cumulativeWeight.last > 0, "Sum of the sampling weights is not positive")
var weightIdx = 0
var weight = 0
def next(): Stream[T] = {
val tmpIdx = weightIdx
weight = weight + 1
weight = if (weight >= weights(weightIdx)) 0 else weight
weightIdx = if (weight == 0) weightIdx + 1 else weightIdx
weightIdx = if (weightIdx == weights.length) 0 else weightIdx
items(tmpIdx) #:: next()
}
next()
}
}
}

View File

@ -1,88 +0,0 @@
package com.twitter.follow_recommendations.common.utils
import scala.util.Random
object RandomUtil {
/**
* Takes a seq of items which have weights. Returns an infinite stream that is
* sampled with replacement using the weights for each item. All weights need
* to be greater than or equal to zero. In addition, the sum of weights
* should be greater than zero.
*
* @param items items
* @param weighted provides weights for items
* @tparam T type of item
* @return Stream of Ts
*/
def weightedRandomSamplingWithReplacement[T](
items: Seq[T],
random: Option[Random] = None
)(
implicit weighted: Weighted[T]
): Stream[T] = {
if (items.isEmpty) {
Stream.empty
} else {
val weights = items.map { i => weighted(i) }
assert(weights.forall { _ >= 0 }, "Negative weight exists for sampling")
val cumulativeWeight = weights.scanLeft(0.0)(_ + _).tail
assert(cumulativeWeight.last > 0, "Sum of the sampling weights is not positive")
val cumulativeProbability = cumulativeWeight map (_ / cumulativeWeight.last)
def next(): Stream[T] = {
val rand = random.getOrElse(Random).nextDouble()
val idx = cumulativeProbability.indexWhere(_ >= rand)
items(if (idx == -1) items.length - 1 else idx) #:: next()
}
next()
}
}
/**
* Takes a seq of items and their weights. Returns a lazy weighted shuffle of
* the elements in the list. All weights should be greater than zero.
*
* @param items items
* @param weighted provides weights for items
* @tparam T type of item
* @return Stream of Ts
*/
def weightedRandomShuffle[T](
items: Seq[T],
random: Option[Random] = None
)(
implicit weighted: Weighted[T]
): Stream[T] = {
assert(items.forall { i => weighted(i) > 0 }, "Non-positive weight exists for shuffling")
def next(it: Seq[T]): Stream[T] = {
if (it.isEmpty)
Stream.empty
else {
val cumulativeWeight = it.scanLeft(0.0)((acc: Double, curr: T) => acc + weighted(curr)).tail
val cutoff = random.getOrElse(Random).nextDouble() * cumulativeWeight.last
val idx = cumulativeWeight.indexWhere(_ >= cutoff)
val (left, right) = it.splitAt(idx)
it(if (idx == -1) it.size - 1 else idx) #:: next(left ++ right.drop(1))
}
}
next(items)
}
/**
* Takes a seq of items and a weight function, returns a lazy weighted shuffle of
* the elements in the list.The weight function is based on the rank of the element
* in the original lst.
* @param items
* @param rankToWeight
* @param random
* @tparam T
* @return
*/
def weightedRandomShuffleByRank[T](
items: Seq[T],
rankToWeight: Int => Double,
random: Option[Random] = None
): Stream[T] = {
val candWeights = items.zipWithIndex.map { case (item, rank) => (item, rankToWeight(rank)) }
RandomUtil.weightedRandomShuffle(candWeights, random).map(_._1)
}
}

View File

@ -1,50 +0,0 @@
package com.twitter.follow_recommendations.common.utils
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.base.StatsUtil
import com.twitter.stitch.Stitch
import com.twitter.util.Duration
import com.twitter.util.TimeoutException
object RescueWithStatsUtils {
def rescueWithStats[T](
s: Stitch[Seq[T]],
stats: StatsReceiver,
source: String
): Stitch[Seq[T]] = {
StatsUtil.profileStitchSeqResults(s, stats.scope(source)).rescue {
case _: Exception => Stitch.Nil
}
}
def rescueOptionalWithStats[T](
s: Stitch[Option[T]],
stats: StatsReceiver,
source: String
): Stitch[Option[T]] = {
StatsUtil.profileStitchOptionalResults(s, stats.scope(source)).rescue {
case _: Exception => Stitch.None
}
}
def rescueWithStatsWithin[T](
s: Stitch[Seq[T]],
stats: StatsReceiver,
source: String,
timeout: Duration
): Stitch[Seq[T]] = {
val hydratedScopeSource = stats.scope(source)
StatsUtil
.profileStitchSeqResults(
s.within(timeout)(com.twitter.finagle.util.DefaultTimer),
hydratedScopeSource)
.rescue {
case _: TimeoutException =>
hydratedScopeSource.counter("timeout").incr()
Stitch.Nil
case _: Exception =>
hydratedScopeSource.counter("exception").incr()
Stitch.Nil
}
}
}

View File

@ -1,14 +0,0 @@
package com.twitter.follow_recommendations.common.utils
import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext
import com.twitter.snowflake.id.SnowflakeId
import com.twitter.util.Duration
import com.twitter.util.Time
object UserSignupUtil {
def signupTime(hasClientContext: HasClientContext): Option[Time] =
hasClientContext.clientContext.userId.flatMap(SnowflakeId.timeFromIdOpt)
def userSignupAge(hasClientContext: HasClientContext): Option[Duration] =
signupTime(hasClientContext).map(Time.now - _)
}

View File

@ -1,21 +0,0 @@
package com.twitter.follow_recommendations.common.utils
/**
* Typeclass for any Recommendation type that has a weight
*
*/
trait Weighted[-Rec] {
def apply(rec: Rec): Double
}
object Weighted {
implicit object WeightedTuple extends Weighted[(_, Double)] {
override def apply(rec: (_, Double)): Double = rec._2
}
def fromFunction[Rec](f: Rec => Double): Weighted[Rec] = {
new Weighted[Rec] {
override def apply(rec: Rec): Double = f(rec)
}
}
}

View File

@ -1,20 +0,0 @@
resources(
sources = [
"*.tsv",
"*.xml",
"**/*",
"config/*.yml",
],
)
# Created for Bazel compatibility.
# In Bazel, loose files must be part of a target to be included into a bundle.
files(
name = "frs_resources",
sources = [
"*.tsv",
"*.xml",
"*.yml",
"**/*",
],
)

View File

@ -1,129 +0,0 @@
enable_recommendations:
comment: Proportion of requests where we return an actual response as part. Decreasing the value will increase the portion of empty responses (in order to disable the service) as part of the graceful degradation.
default_availability: 10000
enable_score_user_candidates:
comment: Proportion of requests where score user candidates from the scoreUserCandidates endpoint
default_availability: 10000
enable_profile_sidebar_product:
comment: Proportion of requests where we return an actual response for profile sidebar product
default_availability: 10000
enable_magic_recs_product:
comment: Proportion of requests where we return an actual response for magic recs product
default_availability: 10000
enable_rux_landing_page_product:
comment: Proportion of requests where we return an actual response for rux landing page product
default_availability: 10000
enable_rux_pymk_product:
comment: Proportion of requests where we return an actual response for rux pymk product
default_availability: 10000
enable_profile_bonus_follow_product:
comment: Proportion of requests where we return an actual response for profile bonus follow product
default_availability: 10000
enable_election_explore_wtf_product:
comment: Proportion of requests where we return an actual response for election explore wtf product
default_availability: 10000
enable_cluster_follow_product:
comment: Proportion of requests where we return an actual response for cluster follow product
default_availability: 10000
enable_home_timeline_product:
comment: Proportion of requests where we return an actual response for htl wtf product
default_availability: 10000
enable_htl_bonus_follow_product:
comment: Proportion of requests where we return an actual response for htl bonus follow product
default_availability: 10000
enable_explore_tab_product:
comment: Proportion of requests where we return an actual response for explore tab product
default_availability: 10000
enable_sidebar_product:
comment: Proportion of requests where we return an actual response for sidebar product
default_availability: 10000
enable_campaign_form_product:
comment: Proportion of requests where we return an actual response for campaign form product
default_availability: 10000
enable_reactive_follow_product:
comment: Proportion of requests where we return an actual response for reactive follow product
default_availability: 10000
enable_nux_pymk_product:
comment: Proportion of requests where we return an actual response for nux pymk product
default_availability: 10000
enable_nux_interests_product:
comment: Proportion of requests where we return an actual response for nux interests product
default_availability: 10000
enable_nux_topic_bonus_follow_product:
comment: Proportion of requests where we return an actual response for nux topic-based bonus follow product
default_availability: 10000
enable_india_covid19_curated_accounts_wtf_product:
comment: Proportion of requests where we return an actual response for india covid19 curated accounts wtf product
default_availability: 10000
enable_ab_upload_product:
comment: Proportion of requests where we return an actual response for the address book upload product
default_availability: 10000
enable_people_plus_plus_product:
comment: Proportion of requests where we return an actual response for the PeoplePlusPlus/Connect Tab product
default_availability: 10000
enable_tweet_notification_recs_product:
comment: Proportion of requests where we return an actual response for the Tweet Notification Recommendations product
default_availability: 10000
enable_profile_device_follow_product:
comment: Proportion of requests where we return an actual response for the ProfileDeviceFollow product
default_availability: 10000
enable_diffy_module_dark_reading:
comment: Percentage of dark read traffic routed to diffy thrift
default_availability: 0
enable_recos_backfill_product:
comment: Proportion of requests where we return an actual response for the RecosBackfill product
default_availability: 10000
enable_post_nux_follow_task_product:
comment: Proportion of requests where we return an actual response for post NUX follow task product
default_availability: 10000
enable_curated_space_hosts_product:
comment: Proportion of requests where we return an actual response for curated space hosts product
default_availability: 10000
enable_nux_geo_category_product:
comment: Proportion of requests where we return an actual response for nux geo category product
default_availability: 10000
enable_nux_interests_category_product:
comment: Proportion of requests where we return an actual response for nux interests category product
default_availability: 10000
enable_nux_pymk_category_product:
comment: Proportion of requests where we return an actual response for nux pymk category product
default_availability: 10000
enable_home_timeline_tweet_recs_product:
comment: Proportion of requests where we return an actual response for the Home Timeline Tweet Recs product
default_availability: 10000
enable_htl_bulk_friend_follows_product:
comment: Proportion of requests where we return an actual response for the HTL bulk friend follows product
default_availability: 10000
enable_nux_auto_follow_product:
comment: Proportion of requests where we return an actual response for the NUX auto follow product
default_availability: 10000
enable_search_bonus_follow_product:
comment: Proportion of requests where we return an actual response for search bonus follow product
default_availability: 10000
enable_fetch_user_in_request_builder:
comment: Proportion of requests where we fetch user object from gizmoduck in request builder
default_availability: 0
enable_product_mixer_magic_recs_product:
comment: Proportion of requests where we enable the product mixer magic recs product
default_availability: 10000
enable_home_timeline_reverse_chron_product:
comment: Proportion of requests where we return an actual response for Home timeline reverse chron product
default_availability: 10000
enable_product_mixer_pipeline_magic_recs_dark_read:
comment: Compare product mixer pipeline responses to current FRS pipeline responses for Magic Recs
default_availability: 0
enable_experimental_caching:
comment: Proportion of requests we use experimental caching for data caching
default_availability: 0
enable_distributed_caching:
comment: Proportion of requests we use a distributed cache cluster for data caching
default_availability: 10000
enable_gizmoduck_caching:
comment: Proportion of requests we use a distributed cache cluster for data caching in Gizmoduck
default_availability: 10000
enable_traffic_dark_reading:
comment: Proportion of requests where we replicate the request for traffic dark reading
default_availability: 0
enable_graph_feature_service_requests:
comment: Proportion of requests where we allow request calls to Graph Feature Service
default_availability: 10000

View File

@ -1,133 +0,0 @@
<configuration>
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<!-- ===================================================== -->
<!-- Service Config -->
<!-- ===================================================== -->
<property name="DEFAULT_SERVICE_PATTERN"
value="%-16X{traceId} %-12X{serviceIdentifier:--} %-16X{method} %-12X{product:--} %-25logger{0} %msg"/>
<property name="DEFAULT_ACCESS_PATTERN"
value="%msg %-12X{serviceIdentifier:--} %X{traceId} %X{product:--}"/>
<!-- ===================================================== -->
<!-- Common Config -->
<!-- ===================================================== -->
<!-- JUL/JDK14 to Logback bridge -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<!-- Service Log (Rollover every 50MB, max 5 logs) -->
<appender name="SERVICE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.service.output}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${log.service.output}.%i</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>5</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>50MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%date %.-3level ${DEFAULT_SERVICE_PATTERN}%n</pattern>
</encoder>
</appender>
<!-- Access Log (Rollover every 50MB, max 5 logs) -->
<appender name="ACCESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.access.output}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${log.access.output}.%i</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>5</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>50MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>${DEFAULT_ACCESS_PATTERN}%n</pattern>
</encoder>
</appender>
<!--LogLens -->
<appender name="LOGLENS" class="com.twitter.loglens.logback.LoglensAppender">
<mdcAdditionalContext>true</mdcAdditionalContext>
<category>${log.lens.category}</category>
<index>${log.lens.index}</index>
<tag>${log.lens.tag}/service</tag>
<encoder>
<pattern>%msg</pattern>
</encoder>
</appender>
<!-- LogLens Access -->
<appender name="LOGLENS-ACCESS" class="com.twitter.loglens.logback.LoglensAppender">
<mdcAdditionalContext>true</mdcAdditionalContext>
<category>${log.lens.category}</category>
<index>${log.lens.index}</index>
<tag>${log.lens.tag}/access</tag>
<encoder>
<pattern>%msg</pattern>
</encoder>
</appender>
<!-- ===================================================== -->
<!-- Primary Async Appenders -->
<!-- ===================================================== -->
<property name="async_queue_size" value="${queue.size:-50000}"/>
<property name="async_max_flush_time" value="${max.flush.time:-0}"/>
<appender name="ASYNC-SERVICE" class="com.twitter.inject.logback.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="SERVICE"/>
</appender>
<appender name="ASYNC-ACCESS" class="com.twitter.inject.logback.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="ACCESS"/>
</appender>
<appender name="ASYNC-LOGLENS" class="com.twitter.inject.logback.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="LOGLENS"/>
</appender>
<appender name="ASYNC-LOGLENS-ACCESS" class="com.twitter.inject.logback.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="LOGLENS-ACCESS"/>
</appender>
<!-- ===================================================== -->
<!-- Package Config -->
<!-- ===================================================== -->
<!-- Per-Package Config -->
<logger name="com.twitter" level="info"/>
<logger name="com.twitter.wilyns" level="warn"/>
<logger name="com.twitter.finagle.mux" level="warn"/>
<logger name="com.twitter.finagle.serverset2" level="warn"/>
<logger name="com.twitter.logging.ScribeHandler" level="warn"/>
<logger name="com.twitter.zookeeper.client.internal" level="warn"/>
<!-- Root Config -->
<root level="${log_level:-INFO}">
<appender-ref ref="ASYNC-SERVICE"/>
<appender-ref ref="ASYNC-LOGLENS"/>
</root>
<!-- Access Logging -->
<logger name="com.twitter.finatra.thrift.filters.AccessLoggingFilter"
level="info"
additivity="false">
<appender-ref ref="ASYNC-ACCESS"/>
<appender-ref ref="ASYNC-LOGLENS-ACCESS"/>
</logger>
</configuration>

View File

@ -1,8 +0,0 @@
# OWNER = jdeng
# Date = 20141223_153423
# Training Size = 16744473
# Testing Size = 16767335
# trained with ElasticNetCV alpha=0.05 cv_folds=5 best_lambda=1.0E-7
# num base features: 10
# num nonzero weights: 30
{bias:-5.67151,featureMetadataMap:["fwd_email":{metadata:{featureWeight:{weight:2.47389}}},"rev_phone":{metadata:{featureWeight:{weight:1.88433}}},"mutual_follow_path":{metadata:{featureWeight:{intervalWeights:[{left:47.0,weight:6.31809},{left:11.0,right:16.0,weight:4.52959},{left:31.0,right:47.0,weight:5.7101},{right:2.0,weight:0.383515},{left:24.0,right:31.0,weight:5.26515},{left:3.0,right:4.0,weight:2.91751},{left:2.0,right:3.0,weight:2.22851},{left:4.0,right:5.0,weight:3.28515},{left:8.0,right:11.0,weight:4.14731},{left:5.0,right:8.0,weight:3.73588},{left:16.0,right:24.0,weight:4.90908}]}}},"fwd_phone":{metadata:{featureWeight:{weight:2.07327}}},"fwd_email_path":{metadata:{featureWeight:{weight:0.961773}}},"rev_phone_path":{metadata:{featureWeight:{weight:0.354484}}},"low_tweepcred_follow_path":{metadata:{featureWeight:{intervalWeights:[{left:4.0,right:5.0,weight:0.177209},{left:7.0,right:8.0,weight:0.12378},{left:3.0,right:4.0,weight:0.197566},{left:5.0,right:6.0,weight:0.15867},{left:2.0,right:3.0,weight:0.196539},{right:2.0,weight:0.1805},{left:75.0,weight:-0.424598},{left:6.0,right:7.0,weight:0.143698},{left:10.0,right:13.0,weight:0.0458502},{left:8.0,right:10.0,weight:0.0919314},{left:13.0,right:75.0,weight:-0.111484}]}}},"rev_email_path":{metadata:{featureWeight:{weight:0.654451}}},"rev_email":{metadata:{featureWeight:{weight:2.33859}}},"fwd_phone_path":{metadata:{featureWeight:{weight:0.210418}}}]}

View File

@ -1 +0,0 @@
{input:{context:"discover.prod",startDateTime:"",endDateTime:"",trainingFeatures:["STP_FEATURES":["fwd_email","mutual_follow_path","fwd_email_path","rev_phone_path","low_tweepcred_follow_path","rev_phone","fwd_phone","rev_email_path","rev_email","fwd_phone_path"]],engagementActions:["click","favorite","open_link","open","send_tweet","send_reply","retweet","reply","profile_click","follow"],impressionActions:["discard","results","impression"],dataFormat:1,dataPath:"",isLabeled:0},sample:{positiveSampleRatio:1.0,negativeSampleRatio:1.0,sampleType:1},split:{trainingDataSplitSize:0.5,testingDataSplitSize:0.5,splitType:2},transform:{},filter:{featureOptions:[]},join:{engagementRules:["discover"],contentIdType:"tweet",groupBucketSize:3600000},discretize:{}}

View File

@ -1,48 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/org/slf4j:slf4j-api",
"finagle/finagle-http/src/main/scala",
"finagle/finagle-thriftmux/src/main/scala",
"finatra-internal/decider/src/main/scala",
"finatra-internal/international/src/main/scala/com/twitter/finatra/international/modules",
"finatra-internal/mtls-http/src/main/scala",
"finatra-internal/mtls-thriftmux/src/main/scala",
"finatra/http-core/src/main/java/com/twitter/finatra/http",
"finatra/inject/inject-app/src/main/scala",
"finatra/inject/inject-core/src/main/scala",
"finatra/inject/inject-server/src/main/scala",
"finatra/inject/inject-thrift-client",
"finatra/jackson/src/main/scala/com/twitter/finatra/jackson/modules",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato",
"follow-recommendations-service/server/src/main/resources",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions",
"follow-recommendations-service/thrift/src/main/thrift:thrift-scala",
"geoduck/service/src/main/scala/com/twitter/geoduck/service/common/clientmodules",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/controllers",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/configapi",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module/stringcenter",
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice",
"twitter-server/server/src/main/scala",
"util/util-app/src/main/scala",
"util/util-core:scala",
"util/util-slf4j-api/src/main/scala",
],
)

View File

@ -1,118 +0,0 @@
package com.twitter.follow_recommendations
import com.google.inject.Module
import com.twitter.finagle.ThriftMux
import com.twitter.finatra.decider.modules.DeciderModule
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
import com.twitter.finatra.international.modules.I18nFactoryModule
import com.twitter.finatra.international.modules.LanguagesModule
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
import com.twitter.finatra.mtls.http.{Mtls => HttpMtls}
import com.twitter.finatra.mtls.thriftmux.Mtls
import com.twitter.finatra.thrift.ThriftServer
import com.twitter.finatra.thrift.filters._
import com.twitter.finagle.thrift.Protocols
import com.twitter.finatra.thrift.routing.ThriftRouter
import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookModule
import com.twitter.follow_recommendations.common.clients.adserver.AdserverModule
import com.twitter.follow_recommendations.common.clients.cache.MemcacheModule
import com.twitter.follow_recommendations.common.clients.deepbirdv2.DeepBirdV2PredictionServiceClientModule
import com.twitter.follow_recommendations.common.clients.email_storage_service.EmailStorageServiceModule
import com.twitter.follow_recommendations.common.clients.geoduck.LocationServiceModule
import com.twitter.follow_recommendations.common.clients.gizmoduck.GizmoduckModule
import com.twitter.follow_recommendations.common.clients.graph_feature_service.GraphFeatureStoreModule
import com.twitter.follow_recommendations.common.clients.impression_store.ImpressionStoreModule
import com.twitter.follow_recommendations.common.clients.phone_storage_service.PhoneStorageServiceModule
import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphModule
import com.twitter.follow_recommendations.common.clients.strato.StratoClientModule
import com.twitter.follow_recommendations.common.constants.ServiceConstants._
import com.twitter.follow_recommendations.common.feature_hydration.sources.HydrationSourcesModule
import com.twitter.follow_recommendations.controllers.ThriftController
import com.twitter.follow_recommendations.modules._
import com.twitter.follow_recommendations.service.exceptions.UnknownLoggingExceptionMapper
import com.twitter.follow_recommendations.services.FollowRecommendationsServiceWarmupHandler
import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService
import com.twitter.geoduck.service.common.clientmodules.ReverseGeocoderThriftClientModule
import com.twitter.inject.thrift.filters.DarkTrafficFilter
import com.twitter.inject.thrift.modules.ThriftClientIdModule
import com.twitter.product_mixer.core.controllers.ProductMixerController
import com.twitter.product_mixer.core.module.PipelineExecutionLoggerModule
import com.twitter.product_mixer.core.module.product_mixer_flags.ProductMixerFlagModule
import com.twitter.product_mixer.core.module.stringcenter.ProductScopeStringCenterModule
import com.twitter.product_mixer.core.product.guice.ProductScopeModule
object FollowRecommendationsServiceThriftServerMain extends FollowRecommendationsServiceThriftServer
class FollowRecommendationsServiceThriftServer
extends ThriftServer
with Mtls
with HttpServer
with HttpMtls {
override val name: String = "follow-recommendations-service-server"
override val modules: Seq[Module] =
Seq(
ABDeciderModule,
AddressbookModule,
AdserverModule,
ConfigApiModule,
DeciderModule,
DeepBirdV2PredictionServiceClientModule,
DiffyModule,
EmailStorageServiceModule,
FeaturesSwitchesModule,
FlagsModule,
GizmoduckModule,
GraphFeatureStoreModule,
HydrationSourcesModule,
I18nFactoryModule,
ImpressionStoreModule,
LanguagesModule,
LocationServiceModule,
MemcacheModule,
PhoneStorageServiceModule,
PipelineExecutionLoggerModule,
ProductMixerFlagModule,
ProductRegistryModule,
new ProductScopeModule(),
new ProductScopeStringCenterModule(),
new ReverseGeocoderThriftClientModule,
ScalaObjectMapperModule,
ScorerModule,
ScribeModule,
SocialGraphModule,
StratoClientModule,
ThriftClientIdModule,
TimerModule,
)
def configureThrift(router: ThriftRouter): Unit = {
router
.filter[LoggingMDCFilter]
.filter[TraceIdMDCFilter]
.filter[ThriftMDCFilter]
.filter[StatsFilter]
.filter[AccessLoggingFilter]
.filter[ExceptionMappingFilter]
.exceptionMapper[UnknownLoggingExceptionMapper]
.filter[DarkTrafficFilter[FollowRecommendationsThriftService.ReqRepServicePerEndpoint]]
.add[ThriftController]
}
override def configureThriftServer(server: ThriftMux.Server): ThriftMux.Server = {
server.withProtocolFactory(
Protocols.binaryFactory(
stringLengthLimit = StringLengthLimit,
containerLengthLimit = ContainerLengthLimit))
}
override def configureHttp(router: HttpRouter): Unit = router.add(
ProductMixerController[FollowRecommendationsThriftService.MethodPerEndpoint](
this.injector,
FollowRecommendationsThriftService.ExecutePipeline))
override def warmup(): Unit = {
handle[FollowRecommendationsServiceWarmupHandler]()
}
}

View File

@ -1,9 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
case class Action(text: String, actionURL: String) {
lazy val toThrift: t.Action = {
t.Action(text, actionURL)
}
}

View File

@ -1,12 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/thrift/src/main/thrift:thrift-scala",
"stringcenter/client",
],
exports = [
],
)

View File

@ -1,8 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.stringcenter.client.core.ExternalString
case class HeaderConfig(title: TitleConfig)
case class TitleConfig(text: ExternalString)
case class FooterConfig(actionConfig: Option[ActionConfig])
case class ActionConfig(footerText: ExternalString, actionURL: String)

View File

@ -1,13 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
trait FeedbackAction {
def toThrift: t.FeedbackAction
}
case class DismissUserId() extends FeedbackAction {
override lazy val toThrift: t.FeedbackAction = {
t.FeedbackAction.DismissUserId(t.DismissUserId())
}
}

View File

@ -1,9 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
case class Footer(action: Option[Action]) {
lazy val toThrift: t.Footer = {
t.Footer(action.map(_.toThrift))
}
}

View File

@ -1,9 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
case class Header(title: Title) {
lazy val toThrift: t.Header = {
t.Header(title.toThrift)
}
}

View File

@ -1,16 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
sealed trait Layout
case class UserListLayout(
header: Option[HeaderConfig],
userListOptions: UserListOptions,
socialProofs: Option[Seq[SocialProof]],
footer: Option[FooterConfig])
extends Layout
case class CarouselLayout(
header: Option[HeaderConfig],
carouselOptions: CarouselOptions,
socialProofs: Option[Seq[SocialProof]])
extends Layout

View File

@ -1,11 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
sealed trait RecommendationOptions
case class UserListOptions(
userBioEnabled: Boolean,
userBioTruncated: Boolean,
userBioMaxLines: Option[Long],
) extends RecommendationOptions
case class CarouselOptions() extends RecommendationOptions

View File

@ -1,16 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.stringcenter.client.core.ExternalString
sealed trait SocialProof
case class GeoContextProof(popularInCountryText: ExternalString) extends SocialProof
case class FollowedByUsersProof(text1: ExternalString, text2: ExternalString, textN: ExternalString)
extends SocialProof
sealed trait SocialText {
def text: String
}
case class GeoSocialText(text: String) extends SocialText
case class FollowedByUsersText(text: String) extends SocialText

View File

@ -1,9 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
case class Title(text: String) {
lazy val toThrift: t.Title = {
t.Title(text)
}
}

View File

@ -1,47 +0,0 @@
package com.twitter.follow_recommendations.assembler.models
import com.twitter.follow_recommendations.{thriftscala => t}
trait WTFPresentation {
def toThrift: t.WTFPresentation
}
case class UserList(
userBioEnabled: Boolean,
userBioTruncated: Boolean,
userBioMaxLines: Option[Long],
feedbackAction: Option[FeedbackAction])
extends WTFPresentation {
def toThrift: t.WTFPresentation = {
t.WTFPresentation.UserBioList(
t.UserList(userBioEnabled, userBioTruncated, userBioMaxLines, feedbackAction.map(_.toThrift)))
}
}
object UserList {
def fromUserListOptions(
userListOptions: UserListOptions
): UserList = {
UserList(
userListOptions.userBioEnabled,
userListOptions.userBioTruncated,
userListOptions.userBioMaxLines,
None)
}
}
case class Carousel(
feedbackAction: Option[FeedbackAction])
extends WTFPresentation {
def toThrift: t.WTFPresentation = {
t.WTFPresentation.Carousel(t.Carousel(feedbackAction.map(_.toThrift)))
}
}
object Carousel {
def fromCarouselOptions(
carouselOptions: CarouselOptions
): Carousel = {
Carousel(None)
}
}

View File

@ -1,16 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/google/inject:guice",
"3rdparty/jvm/com/google/inject/extensions:guice-assistedinject",
"3rdparty/jvm/net/codingwell:scala-guice",
"3rdparty/jvm/org/slf4j:slf4j-api",
"finatra/inject/inject-core/src/main/scala",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models",
"util/util-slf4j-api/src/main/scala",
],
)

View File

@ -1,138 +0,0 @@
package com.twitter.follow_recommendations.blenders
import com.google.common.annotations.VisibleForTesting
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.base.Transform
import com.twitter.follow_recommendations.common.models.AdMetadata
import com.twitter.follow_recommendations.common.models.Recommendation
import com.twitter.inject.Logging
import com.twitter.stitch.Stitch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PromotedAccountsBlender @Inject() (statsReceiver: StatsReceiver)
extends Transform[Int, Recommendation]
with Logging {
import PromotedAccountsBlender._
val stats = statsReceiver.scope(Name)
val inputOrganicAccounts = stats.counter(InputOrganic)
val inputPromotedAccounts = stats.counter(InputPromoted)
val outputOrganicAccounts = stats.counter(OutputOrganic)
val outputPromotedAccounts = stats.counter(OutputPromoted)
val promotedAccountsStats = stats.scope(NumPromotedAccounts)
override def transform(
maxResults: Int,
items: Seq[Recommendation]
): Stitch[Seq[Recommendation]] = {
val (promoted, organic) = items.partition(_.isPromotedAccount)
val promotedIds = promoted.map(_.id).toSet
val dedupedOrganic = organic.filterNot(u => promotedIds.contains(u.id))
val blended = blendPromotedAccount(dedupedOrganic, promoted, maxResults)
val (outputPromoted, outputOrganic) = blended.partition(_.isPromotedAccount)
inputOrganicAccounts.incr(dedupedOrganic.size)
inputPromotedAccounts.incr(promoted.size)
outputOrganicAccounts.incr(outputOrganic.size)
val size = outputPromoted.size
outputPromotedAccounts.incr(size)
if (size <= 5) {
promotedAccountsStats.counter(outputPromoted.size.toString).incr()
} else {
promotedAccountsStats.counter(MoreThan5Promoted).incr()
}
Stitch.value(blended)
}
/**
* Merge Promoted results and organic results. Promoted result dictates the position
* in the merge list.
*
* merge a list of positioned users, aka. promoted, and a list of organic
* users. The positioned promoted users are pre-sorted with regards to their
* position ascendingly. Only requirement about position is to be within the
* range, i.e, can not exceed the combined length if merge is successful, ok
* to be at the last position, but not beyond.
* For more detailed description of location position:
* http://confluence.local.twitter.com/display/ADS/Promoted+Tweets+in+Timeline+Design+Document
*/
@VisibleForTesting
private[blenders] def mergePromotedAccounts(
organicUsers: Seq[Recommendation],
promotedUsers: Seq[Recommendation]
): Seq[Recommendation] = {
def mergeAccountWithIndex(
organicUsers: Seq[Recommendation],
promotedUsers: Seq[Recommendation],
index: Int
): Stream[Recommendation] = {
if (promotedUsers.isEmpty) organicUsers.toStream
else {
val promotedHead = promotedUsers.head
val promotedTail = promotedUsers.tail
promotedHead.adMetadata match {
case Some(AdMetadata(position, _)) =>
if (position < 0) mergeAccountWithIndex(organicUsers, promotedTail, index)
else if (position == index)
promotedHead #:: mergeAccountWithIndex(organicUsers, promotedTail, index)
else if (organicUsers.isEmpty) organicUsers.toStream
else {
val organicHead = organicUsers.head
val organicTail = organicUsers.tail
organicHead #:: mergeAccountWithIndex(organicTail, promotedUsers, index + 1)
}
case _ =>
logger.error("Unknown Candidate type in mergePromotedAccounts")
Stream.empty
}
}
}
mergeAccountWithIndex(organicUsers, promotedUsers, 0)
}
private[this] def blendPromotedAccount(
organic: Seq[Recommendation],
promoted: Seq[Recommendation],
maxResults: Int
): Seq[Recommendation] = {
val merged = mergePromotedAccounts(organic, promoted)
val mergedServed = merged.take(maxResults)
val promotedServed = promoted.intersect(mergedServed)
if (isBlendPromotedNeeded(
mergedServed.size - promotedServed.size,
promotedServed.size,
maxResults
)) {
mergedServed
} else {
organic.take(maxResults)
}
}
@VisibleForTesting
private[blenders] def isBlendPromotedNeeded(
organicSize: Int,
promotedSize: Int,
maxResults: Int
): Boolean = {
(organicSize > 1) &&
(promotedSize > 0) &&
(promotedSize < organicSize) &&
(promotedSize <= 2) &&
(maxResults > 1)
}
}
object PromotedAccountsBlender {
val Name = "promoted_accounts_blender"
val InputOrganic = "input_organic_accounts"
val InputPromoted = "input_promoted_accounts"
val OutputOrganic = "output_organic_accounts"
val OutputPromoted = "output_promoted_accounts"
val NumPromotedAccounts = "num_promoted_accounts"
val MoreThan5Promoted = "more_than_5"
}

View File

@ -1,28 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/google/inject:guice",
"configapi/configapi-core",
"configapi/configapi-decider",
"configapi/configapi-featureswitches:v2",
"featureswitches/featureswitches-core",
"featureswitches/featureswitches-core:v2",
"featureswitches/featureswitches-core/src/main/scala/com/twitter/featureswitches/v2/builder",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products",
],
)

View File

@ -1,16 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.timelines.configapi.CompositeConfig
import com.twitter.timelines.configapi.Config
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ConfigBuilder @Inject() (
deciderConfigs: DeciderConfigs,
featureSwitchConfigs: FeatureSwitchConfigs) {
// The order of configs added to `CompositeConfig` is important. The config will be matched with
// the first possible rule. So, current setup will give priority to Deciders instead of FS
def build(): Config =
new CompositeConfig(Seq(deciderConfigs.config, featureSwitchConfigs.config))
}

View File

@ -1,52 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.decider.Recipient
import com.twitter.decider.SimpleRecipient
import com.twitter.follow_recommendations.configapi.deciders.DeciderKey
import com.twitter.follow_recommendations.configapi.deciders.DeciderParams
import com.twitter.follow_recommendations.products.home_timeline_tweet_recs.configapi.HomeTimelineTweetRecsParams
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.timelines.configapi._
import com.twitter.timelines.configapi.decider.DeciderSwitchOverrideValue
import com.twitter.timelines.configapi.decider.GuestRecipient
import com.twitter.timelines.configapi.decider.RecipientBuilder
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DeciderConfigs @Inject() (deciderGateBuilder: DeciderGateBuilder) {
val overrides: Seq[OptionalOverride[_]] = DeciderConfigs.ParamsToDeciderMap.map {
case (params, deciderKey) =>
params.optionalOverrideValue(
DeciderSwitchOverrideValue(
feature = deciderGateBuilder.keyToFeature(deciderKey),
enabledValue = true,
recipientBuilder = DeciderConfigs.UserOrGuestOrRequest
)
)
}.toSeq
val config: BaseConfig = BaseConfigBuilder(overrides).build("FollowRecommendationServiceDeciders")
}
object DeciderConfigs {
val ParamsToDeciderMap = Map(
DeciderParams.EnableRecommendations -> DeciderKey.EnableRecommendations,
DeciderParams.EnableScoreUserCandidates -> DeciderKey.EnableScoreUserCandidates,
HomeTimelineTweetRecsParams.EnableProduct -> DeciderKey.EnableHomeTimelineTweetRecsProduct,
)
object UserOrGuestOrRequest extends RecipientBuilder {
def apply(requestContext: BaseRequestContext): Option[Recipient] = requestContext match {
case c: WithUserId if c.userId.isDefined =>
c.userId.map(SimpleRecipient)
case c: WithGuestId if c.guestId.isDefined =>
c.guestId.map(GuestRecipient)
case c: WithGuestId =>
RecipientBuilder.Request(c)
case _ =>
throw new UndefinedUserIdNorGuestIDException(requestContext)
}
}
}

View File

@ -1,138 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.candidate_sources.base.SocialProofEnforcedCandidateSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeoQualityFollowSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeoSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.sims.SimsSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.SimsExpansionFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.socialgraph.RecentFollowingRecentFollowingExpansionSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStpSourceFsConfig
import com.twitter.follow_recommendations.common.candidate_sources.stp.OnlineSTPSourceFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsFSConfig
import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphFSConfig
import com.twitter.follow_recommendations.common.feature_hydration.sources.FeatureHydrationSourcesFSConfig
import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.WeightedCandidateSourceRankerFSConfig
import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig
import com.twitter.follow_recommendations.flows.content_recommender_flow.ContentRecommenderFlowFSConfig
import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicateFSConfig
import com.twitter.follow_recommendations.common.predicates.hss.HssPredicateFSConfig
import com.twitter.follow_recommendations.common.predicates.sgs.SgsPredicateFSConfig
import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlFlowFSConfig
import com.twitter.logging.Logger
import com.twitter.timelines.configapi.BaseConfigBuilder
import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FeatureSwitchConfigs @Inject() (
globalFeatureSwitchConfig: GlobalFeatureSwitchConfig,
featureHydrationSourcesFSConfig: FeatureHydrationSourcesFSConfig,
weightedCandidateSourceRankerFSConfig: WeightedCandidateSourceRankerFSConfig,
// Flow related config
contentRecommenderFlowFSConfig: ContentRecommenderFlowFSConfig,
postNuxMlFlowFSConfig: PostNuxMlFlowFSConfig,
// Candidate source related config
crowdSearchAccountsFSConfig: CrowdSearchAccountsFSConfig,
offlineStpSourceFsConfig: OfflineStpSourceFsConfig,
onlineSTPSourceFSConfig: OnlineSTPSourceFSConfig,
popGeoSourceFSConfig: PopGeoSourceFSConfig,
popGeoQualityFollowFSConfig: PopGeoQualityFollowSourceFSConfig,
realGraphOonFSConfig: RealGraphOonFSConfig,
repeatedProfileVisitsFSConfig: RepeatedProfileVisitsFSConfig,
recentEngagementSimilarUsersFSConfig: RecentEngagementSimilarUsersFSConfig,
recentFollowingRecentFollowingExpansionSourceFSConfig: RecentFollowingRecentFollowingExpansionSourceFSConfig,
simsExpansionFSConfig: SimsExpansionFSConfig,
simsSourceFSConfig: SimsSourceFSConfig,
socialProofEnforcedCandidateSourceFSConfig: SocialProofEnforcedCandidateSourceFSConfig,
triangularLoopsFSConfig: TriangularLoopsFSConfig,
userUserGraphFSConfig: UserUserGraphFSConfig,
// Predicate related configs
gizmoduckPredicateFSConfig: GizmoduckPredicateFSConfig,
hssPredicateFSConfig: HssPredicateFSConfig,
sgsPredicateFSConfig: SgsPredicateFSConfig,
ppmiLocaleSourceFSConfig: PPMILocaleFollowSourceFSConfig,
topOrganicFollowsAccountsFSConfig: TopOrganicFollowsAccountsFSConfig,
statsReceiver: StatsReceiver) {
val logger = Logger(classOf[FeatureSwitchConfigs])
val mergedFSConfig =
FeatureSwitchConfig.merge(
Seq(
globalFeatureSwitchConfig,
featureHydrationSourcesFSConfig,
weightedCandidateSourceRankerFSConfig,
// Flow related config
contentRecommenderFlowFSConfig,
postNuxMlFlowFSConfig,
// Candidate source related config
crowdSearchAccountsFSConfig,
offlineStpSourceFsConfig,
onlineSTPSourceFSConfig,
popGeoSourceFSConfig,
popGeoQualityFollowFSConfig,
realGraphOonFSConfig,
repeatedProfileVisitsFSConfig,
recentEngagementSimilarUsersFSConfig,
recentFollowingRecentFollowingExpansionSourceFSConfig,
simsExpansionFSConfig,
simsSourceFSConfig,
socialProofEnforcedCandidateSourceFSConfig,
triangularLoopsFSConfig,
userUserGraphFSConfig,
// Predicate related configs:
gizmoduckPredicateFSConfig,
hssPredicateFSConfig,
sgsPredicateFSConfig,
ppmiLocaleSourceFSConfig,
topOrganicFollowsAccountsFSConfig,
)
)
/**
* enum params have to be listed in this main file together as otherwise we'll have to pass in
* some signature like `Seq[FSEnumParams[_]]` which are generics of generics and won't compile.
* we only have enumFsParams from globalFeatureSwitchConfig at the moment
*/
val enumOverrides = globalFeatureSwitchConfig.enumFsParams.flatMap { enumParam =>
FeatureSwitchOverrideUtil.getEnumFSOverrides(statsReceiver, logger, enumParam)
}
val gatedOverrides = mergedFSConfig.gatedOverridesMap.flatMap {
case (fsName, overrides) =>
FeatureSwitchOverrideUtil.gatedOverrides(fsName, overrides: _*)
}
val enumSeqOverrides = globalFeatureSwitchConfig.enumSeqFsParams.flatMap { enumSeqParam =>
FeatureSwitchOverrideUtil.getEnumSeqFSOverrides(statsReceiver, logger, enumSeqParam)
}
val overrides =
FeatureSwitchOverrideUtil
.getBooleanFSOverrides(mergedFSConfig.booleanFSParams: _*) ++
FeatureSwitchOverrideUtil
.getBoundedIntFSOverrides(mergedFSConfig.intFSParams: _*) ++
FeatureSwitchOverrideUtil
.getBoundedLongFSOverrides(mergedFSConfig.longFSParams: _*) ++
FeatureSwitchOverrideUtil
.getBoundedDoubleFSOverrides(mergedFSConfig.doubleFSParams: _*) ++
FeatureSwitchOverrideUtil
.getDurationFSOverrides(mergedFSConfig.durationFSParams: _*) ++
FeatureSwitchOverrideUtil
.getBoundedOptionalDoubleOverrides(mergedFSConfig.optionalDoubleFSParams: _*) ++
FeatureSwitchOverrideUtil.getStringSeqFSOverrides(mergedFSConfig.stringSeqFSParams: _*) ++
enumOverrides ++
gatedOverrides ++
enumSeqOverrides
val config = BaseConfigBuilder(overrides).build("FollowRecommendationServiceFeatureSwitches")
}

View File

@ -1,49 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsParams.AccountsFilteringAndRankingLogics
import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsParams.{
AccountsFilteringAndRankingLogics => OrganicAccountsFilteringAndRankingLogics
}
import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersParams
import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.SimsExpansionSourceParams
import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.MlRankerParams.CandidateScorerIdParam
import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig
import com.twitter.follow_recommendations.configapi.params.GlobalParams.CandidateSourcesToFilter
import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableCandidateParamHydrations
import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableGFSSocialProofTransform
import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableRecommendationFlowLogs
import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableWhoToFollowProducts
import com.twitter.follow_recommendations.configapi.params.GlobalParams.KeepSocialUserCandidate
import com.twitter.follow_recommendations.configapi.params.GlobalParams.KeepUserCandidate
import com.twitter.timelines.configapi.FSName
import com.twitter.timelines.configapi.Param
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GlobalFeatureSwitchConfig @Inject() () extends FeatureSwitchConfig {
override val booleanFSParams: Seq[Param[Boolean] with FSName] = {
Seq(
EnableCandidateParamHydrations,
KeepUserCandidate,
KeepSocialUserCandidate,
EnableGFSSocialProofTransform,
EnableWhoToFollowProducts,
EnableRecommendationFlowLogs
)
}
val enumFsParams =
Seq(
CandidateScorerIdParam,
SimsExpansionSourceParams.Aggregator,
RecentEngagementSimilarUsersParams.Aggregator,
CandidateSourcesToFilter,
)
val enumSeqFsParams =
Seq(
AccountsFilteringAndRankingLogics,
OrganicAccountsFilteringAndRankingLogics
)
}

View File

@ -1,29 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.follow_recommendations.common.models.DisplayLocation
import com.twitter.product_mixer.core.model.marshalling.request.ClientContext
import com.twitter.servo.util.MemoizingStatsReceiver
import com.twitter.timelines.configapi.Config
import com.twitter.timelines.configapi.FeatureValue
import com.twitter.timelines.configapi.Params
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ParamsFactory @Inject() (
config: Config,
requestContextFactory: RequestContextFactory,
statsReceiver: StatsReceiver) {
private val stats = new MemoizingStatsReceiver(statsReceiver.scope("configapi"))
def apply(followRecommendationServiceRequestContext: RequestContext): Params =
config(followRecommendationServiceRequestContext, stats)
def apply(
clientContext: ClientContext,
displayLocation: DisplayLocation,
featureOverrides: Map[String, FeatureValue]
): Params =
apply(requestContextFactory(clientContext, displayLocation, featureOverrides))
}

View File

@ -1,19 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.twitter.timelines.configapi.BaseRequestContext
import com.twitter.timelines.configapi.FeatureContext
import com.twitter.timelines.configapi.NullFeatureContext
import com.twitter.timelines.configapi.GuestId
import com.twitter.timelines.configapi.UserId
import com.twitter.timelines.configapi.WithFeatureContext
import com.twitter.timelines.configapi.WithGuestId
import com.twitter.timelines.configapi.WithUserId
case class RequestContext(
userId: Option[UserId],
guestId: Option[GuestId],
featureContext: FeatureContext = NullFeatureContext)
extends BaseRequestContext
with WithUserId
with WithGuestId
with WithFeatureContext

View File

@ -1,74 +0,0 @@
package com.twitter.follow_recommendations.configapi
import com.google.common.annotations.VisibleForTesting
import com.google.inject.Inject
import com.twitter.decider.Decider
import com.twitter.featureswitches.v2.FeatureSwitches
import com.twitter.featureswitches.{Recipient => FeatureSwitchRecipient}
import com.twitter.follow_recommendations.common.models.DisplayLocation
import com.twitter.product_mixer.core.model.marshalling.request.ClientContext
import com.twitter.snowflake.id.SnowflakeId
import com.twitter.timelines.configapi.FeatureContext
import com.twitter.timelines.configapi.FeatureValue
import com.twitter.timelines.configapi.ForcedFeatureContext
import com.twitter.timelines.configapi.OrElseFeatureContext
import com.twitter.timelines.configapi.featureswitches.v2.FeatureSwitchResultsFeatureContext
import javax.inject.Singleton
/*
* Request Context Factory is used to build RequestContext objects which are used
* by the config api to determine the param overrides to apply to the request.
* The param overrides are determined per request by configs which specify which
* FS/Deciders/AB translate to what param overrides.
*/
@Singleton
class RequestContextFactory @Inject() (featureSwitches: FeatureSwitches, decider: Decider) {
def apply(
clientContext: ClientContext,
displayLocation: DisplayLocation,
featureOverrides: Map[String, FeatureValue]
): RequestContext = {
val featureContext = getFeatureContext(clientContext, displayLocation, featureOverrides)
RequestContext(clientContext.userId, clientContext.guestId, featureContext)
}
private[configapi] def getFeatureContext(
clientContext: ClientContext,
displayLocation: DisplayLocation,
featureOverrides: Map[String, FeatureValue]
): FeatureContext = {
val recipient =
getFeatureSwitchRecipient(clientContext)
.withCustomFields("display_location" -> displayLocation.toFsName)
// userAgeOpt is going to be set to None for logged out users and defaulted to Some(Int.MaxValue) for non-snowflake users
val userAgeOpt = clientContext.userId.map { userId =>
SnowflakeId.timeFromIdOpt(userId).map(_.untilNow.inDays).getOrElse(Int.MaxValue)
}
val recipientWithAccountAge =
userAgeOpt
.map(age => recipient.withCustomFields("account_age_in_days" -> age)).getOrElse(recipient)
val results = featureSwitches.matchRecipient(recipientWithAccountAge)
OrElseFeatureContext(
ForcedFeatureContext(featureOverrides),
new FeatureSwitchResultsFeatureContext(results))
}
@VisibleForTesting
private[configapi] def getFeatureSwitchRecipient(
clientContext: ClientContext
): FeatureSwitchRecipient = {
FeatureSwitchRecipient(
userId = clientContext.userId,
userRoles = clientContext.userRoles,
deviceId = clientContext.deviceId,
guestId = clientContext.guestId,
languageCode = clientContext.languageCode,
countryCode = clientContext.countryCode,
isVerified = None,
clientApplicationId = clientContext.appId,
isTwoffice = clientContext.isTwoffice
)
}
}

View File

@ -1,18 +0,0 @@
scala_library(
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/google/inject:guice",
"configapi/configapi-core",
"configapi/configapi-decider",
"configapi/configapi-featureswitches:v2",
"featureswitches/featureswitches-core",
"featureswitches/featureswitches-core:v2",
"featureswitches/featureswitches-core/src/main/scala/com/twitter/featureswitches/v2/builder",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base",
"follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders",
"follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params",
],
)

View File

@ -1,19 +0,0 @@
package com.twitter.follow_recommendations.configapi.candidates
import com.twitter.timelines.configapi.BaseRequestContext
import com.twitter.timelines.configapi.FeatureContext
import com.twitter.timelines.configapi.NullFeatureContext
import com.twitter.timelines.configapi.WithFeatureContext
import com.twitter.timelines.configapi.WithUserId
/**
* represent the context for a recommendation candidate (producer side)
* @param userId id of the recommended user
* @param featureContext feature context
*/
case class CandidateUserContext(
override val userId: Option[Long],
featureContext: FeatureContext = NullFeatureContext)
extends BaseRequestContext
with WithUserId
with WithFeatureContext

View File

@ -1,55 +0,0 @@
package com.twitter.follow_recommendations.configapi.candidates
import com.google.common.annotations.VisibleForTesting
import com.google.inject.Inject
import com.twitter.decider.Decider
import com.twitter.featureswitches.v2.FeatureSwitches
import com.twitter.featureswitches.{Recipient => FeatureSwitchRecipient}
import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants.PRODUCER_SIDE_FEATURE_SWITCHES
import com.twitter.follow_recommendations.common.models.CandidateUser
import com.twitter.follow_recommendations.common.models.DisplayLocation
import com.twitter.timelines.configapi.FeatureContext
import com.twitter.timelines.configapi.featureswitches.v2.FeatureSwitchResultsFeatureContext
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class CandidateUserContextFactory @Inject() (
@Named(PRODUCER_SIDE_FEATURE_SWITCHES) featureSwitches: FeatureSwitches,
decider: Decider) {
def apply(
candidateUser: CandidateUser,
displayLocation: DisplayLocation
): CandidateUserContext = {
val featureContext = getFeatureContext(candidateUser, displayLocation)
CandidateUserContext(Some(candidateUser.id), featureContext)
}
private[configapi] def getFeatureContext(
candidateUser: CandidateUser,
displayLocation: DisplayLocation
): FeatureContext = {
val recipient = getFeatureSwitchRecipient(candidateUser).withCustomFields(
"display_location" -> displayLocation.toFsName)
new FeatureSwitchResultsFeatureContext(featureSwitches.matchRecipient(recipient))
}
@VisibleForTesting
private[configapi] def getFeatureSwitchRecipient(
candidateUser: CandidateUser
): FeatureSwitchRecipient = {
FeatureSwitchRecipient(
userId = Some(candidateUser.id),
userRoles = None,
deviceId = None,
guestId = None,
languageCode = None,
countryCode = None,
isVerified = None,
clientApplicationId = None,
isTwoffice = None
)
}
}

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