Twitter Recommendation Algorithm

Please note we have force-pushed a new initial commit in order to remove some publicly-available Twitter user information. Note that this process may be required in the future.
This commit is contained in:
twitter-team
2023-03-31 17:36:31 -05:00
commit ef4c5eb65e
5364 changed files with 460239 additions and 0 deletions

View File

@ -0,0 +1,5 @@
resources(
sources = [
"*.xml",
],
)

View File

@ -0,0 +1,124 @@
<configuration>
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<property name="async_queue_size" value="${queue.size:-50000}"/>
<property name="async_max_flush_time" value="${max.flush.time:-0}"/>
<property name="SERVICE_OUTPUT" value="${log.service.output:-server.log}"/>
<property name="DEBUG_TRANSCRIPTS_OUTPUT"
value="${log.debug_transcripts.output:-debug_transcripts.log}"/>
<property name="DEFAULT_SERVICE_PATTERN"
value="%5p [%d{yyyyMMdd-HH:mm:ss.SSS}] %logger{0}: %m%n"/>
<!-- JUL/JDK14 to Logback bridge -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<!-- Service Log -->
<appender name="SERVICE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${SERVICE_OUTPUT}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${SERVICE_OUTPUT}.%d.%i.gz</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<!-- keep 21 days' worth of history -->
<maxHistory>21</maxHistory>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>${DEFAULT_SERVICE_PATTERN}</pattern>
</encoder>
</appender>
<!-- debug transcripts -->
<appender name="DEBUG-TRANSCRIPTS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${DEBUG_TRANSCRIPTS_OUTPUT}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>${DEBUG_TRANSCRIPTS_OUTPUT}.%d.%i.gz</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<!-- keep 21 days' worth of history -->
<maxHistory>21</maxHistory>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>${DEFAULT_SERVICE_PATTERN}</pattern>
</encoder>
</appender>
<!-- LogLens/splunk -->
<appender name="LOGLENS" class="com.twitter.loglens.logback.LoglensAppender">
<mdcAdditionalContext>true</mdcAdditionalContext>
<category>loglens</category>
<index>${log.lens.index:-timelineranker}</index>
<tag>${log.lens.tag}</tag>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
<filter class="com.twitter.strato.logging.logback.RegexFilter">
<forLogger>manhattan-client</forLogger>
<excludeRegex>.*InvalidRequest.*</excludeRegex>
</filter>
</appender>
<!-- ===================================================== -->
<!-- Primary Async Appenders -->
<!-- ===================================================== -->
<appender name="ASYNC-SERVICE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="SERVICE"/>
</appender>
<appender name="ASYNC-DEBUG-TRANSCRIPTS" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="DEBUG-TRANSCRIPTS"/>
</appender>
<appender name="ASYNC-LOGLENS" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>${async_queue_size}</queueSize>
<maxFlushTime>${async_max_flush_time}</maxFlushTime>
<appender-ref ref="LOGLENS"/>
</appender>
<!-- ===================================================== -->
<!-- Package Config -->
<!-- ===================================================== -->
<!-- Per-Package Config -->
<logger name="OptimisticLockingCache" level="off"/>
<logger name="ZkSession" level="info"/>
<logger name="com.twitter" level="info"/>
<logger name="com.twitter.decider.StoreDecider" level="warn"/>
<logger name="com.twitter.distributedlog.client" level="warn"/>
<logger name="com.twitter.finagle.liveness" level="warn"/>
<logger name="com.twitter.finagle.mtls.authorization.config.AccessControlListConfiguration" level="warn"/>
<logger name="com.twitter.finagle.mux" level="warn"/>
<logger name="com.twitter.finagle.serverset2" level="warn"/>
<logger name="com.twitter.finatra.kafka.common.kerberoshelpers" level="warn"/>
<logger name="com.twitter.finatra.kafka.utils.BootstrapServerUtils" level="warn"/>
<logger name="com.twitter.logging.ScribeHandler" level="warn"/>
<logger name="com.twitter.server.coordinate" level="error"/>
<logger name="com.twitter.wilyns" level="warn"/>
<logger name="com.twitter.zookeeper.client" level="info"/>
<logger name="com.twitter.zookeeper.client.internal" level="warn"/>
<logger name="manhattan-client" level="warn"/>
<logger name="org.apache.kafka.clients.NetworkClient" level="error"/>
<logger name="org.apache.kafka.clients.consumer.internals" level="error"/>
<logger name="org.apache.kafka.clients.producer.internals" level="error"/>
<logger name="org.apache.kafka.common.network" level="warn"/>
<logger name="org.apache.zookeeper" level="error"/>
<logger name="org.apache.zookeeper.ClientCnxn" level="warn"/>
<!-- Root Config -->
<root level="${log_level:-INFO}">
<appender-ref ref="ASYNC-SERVICE"/>
<appender-ref ref="ASYNC-LOGLENS"/>
</root>
<!-- debug transcripts: logger name MUST be c.t.timelines.util.debuglog.DebugLog.DebugTranscriptsLog -->
<logger name="DebugTranscripts" level="info">
<appender-ref ref="ASYNC-DEBUG-TRANSCRIPTS"/>
<appender-ref ref="ASYNC-LOGLENS"/>
</logger>
</configuration>

View File

@ -0,0 +1,32 @@
target(
dependencies = [
"timelineranker/server/src/main/scala/com/twitter/timelineranker/repository",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/server",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/source",
],
)
jvm_binary(
name = "bin",
basename = "timelineranker-server",
main = "com.twitter.timelineranker.server.Main",
runtime_platform = "java11",
tags = ["bazel-compatible"],
dependencies = [
":scala",
"3rdparty/jvm/org/slf4j:jcl-over-slf4j", # [1]
"3rdparty/jvm/org/slf4j:log4j-over-slf4j", # [1]
"loglens/loglens-logback/src/main/scala/com/twitter/loglens/logback", # [2]
"strato/src/main/scala/com/twitter/strato/logging/logback", # [2]
"timelineranker/server/src/main/resources", # [2]
"twitter-server/logback-classic/src/main/scala", #[2]
],
)
# [1] bridge other logging implementations to slf4j-api in addition to JUL
# https://docbird.twitter.biz/core_libraries_guide/logging/twitter_server.html
# without these, c.t.l.Logger become silent/null logger since no proper
# configuration can be found. This can be removed once there are no
# depdency from service to c.t.l.Logger
#
# [2] incur logback implementation

View File

@ -0,0 +1,22 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"cortex-core/thrift/src/main/thrift:thrift-scala",
"cortex-tweet-annotate/service/src/main/thrift:thrift-scala",
"finagle/finagle-memcached/src/main/scala",
"mediaservices/commons/src/main/thrift:thrift-scala",
"servo/repo",
"servo/util/src/main/scala",
"src/thrift/com/twitter/ml/api:data-scala",
"src/thrift/com/twitter/ml/prediction_service:prediction_service-scala",
"timelines/src/main/scala/com/twitter/timelines/model/types",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/util/stats",
"util/util-core:util-core-util",
"util/util-logging/src/main/scala",
"util/util-stats/src/main/scala",
],
)

View File

@ -0,0 +1,113 @@
package com.twitter.timelineranker.clients
import com.twitter.cortex_core.thriftscala.ModelName
import com.twitter.cortex_tweet_annotate.thriftscala._
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.logging.Logger
import com.twitter.mediaservices.commons.mediainformation.thriftscala.CalibrationLevel
import com.twitter.timelines.model.TweetId
import com.twitter.timelines.util.stats.RequestScope
import com.twitter.timelines.util.stats.RequestStats
import com.twitter.timelines.util.stats.ScopedFactory
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.util.Future
object CortexTweetQueryServiceClient {
private[this] val logger = Logger.get(getClass.getSimpleName)
/**
* A tweet is considered safe if Cortex NSFA model gives it a score that is above the threshold.
* Both the score and the threshold are returned in a response from getTweetSignalByIds endpoint.
*/
private def getSafeTweet(
request: TweetSignalRequest,
response: ModelResponseResult
): Option[TweetId] = {
val tweetId = request.tweetId
response match {
case ModelResponseResult(ModelResponseState.Success, Some(tid), Some(modelResponse), _) =>
val prediction = modelResponse.predictions.flatMap(_.headOption)
val score = prediction.map(_.score.score)
val highRecallBucket = prediction.flatMap(_.calibrationBuckets).flatMap { buckets =>
buckets.find(_.description.contains(CalibrationLevel.HighRecall))
}
val threshold = highRecallBucket.map(_.threshold)
(score, threshold) match {
case (Some(s), Some(t)) if (s > t) =>
Some(tid)
case (Some(s), Some(t)) =>
logger.ifDebug(
s"Cortex NSFA score for tweet $tweetId is $s (threshold is $t), removing as unsafe."
)
None
case _ =>
logger.ifDebug(s"Unexpected response, removing tweet $tweetId as unsafe.")
None
}
case _ =>
logger.ifWarning(
s"Cortex tweet NSFA call was not successful, removing tweet $tweetId as unsafe."
)
None
}
}
}
/**
* Enables calling cortex tweet query service to get NSFA scores on the tweet.
*/
class CortexTweetQueryServiceClient(
cortexClient: CortexTweetQueryService.MethodPerEndpoint,
requestScope: RequestScope,
statsReceiver: StatsReceiver)
extends RequestStats {
import CortexTweetQueryServiceClient._
private[this] val logger = Logger.get(getClass.getSimpleName)
private[this] val getTweetSignalByIdsRequestStats =
requestScope.stats("cortex", statsReceiver, suffix = Some("getTweetSignalByIds"))
private[this] val getTweetSignalByIdsRequestScopedStatsReceiver =
getTweetSignalByIdsRequestStats.scopedStatsReceiver
private[this] val failedCortexTweetQueryServiceScope =
getTweetSignalByIdsRequestScopedStatsReceiver.scope(Failures)
private[this] val failedCortexTweetQueryServiceCallCounter =
failedCortexTweetQueryServiceScope.counter("failOpen")
private[this] val cortexTweetQueryServiceFailOpenHandler = new FailOpenHandler(
getTweetSignalByIdsRequestScopedStatsReceiver
)
def getSafeTweets(tweetIds: Seq[TweetId]): Future[Seq[TweetId]] = {
val requests = tweetIds.map { id => TweetSignalRequest(id, ModelName.TweetToNsfa) }
val results = cortexClient
.getTweetSignalByIds(
GetTweetSignalByIdsRequest(requests)
)
.map(_.results)
cortexTweetQueryServiceFailOpenHandler(
results.map { responses =>
requests.zip(responses).flatMap {
case (request, response) =>
getSafeTweet(request, response)
}
}
) { _ =>
failedCortexTweetQueryServiceCallCounter.incr()
logger.ifWarning(s"Cortex tweet NSFA call failed, considering tweets $tweetIds as unsafe.")
Future.value(Seq())
}
}
}
class ScopedCortexTweetQueryServiceClientFactory(
cortexClient: CortexTweetQueryService.MethodPerEndpoint,
statsReceiver: StatsReceiver)
extends ScopedFactory[CortexTweetQueryServiceClient] {
override def scope(scope: RequestScope): CortexTweetQueryServiceClient = {
new CortexTweetQueryServiceClient(cortexClient, scope, statsReceiver)
}
}

View File

@ -0,0 +1,48 @@
package com.twitter.timelineranker.clients
import com.twitter.finagle.memcached.{Client => FinagleMemcacheClient}
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.logging.Logger
import com.twitter.servo.cache.FinagleMemcache
import com.twitter.servo.cache.MemcacheCache
import com.twitter.servo.cache.ObservableMemcache
import com.twitter.servo.cache.Serializer
import com.twitter.servo.cache.StatsReceiverCacheObserver
import com.twitter.timelines.util.stats.RequestScope
import com.twitter.timelines.util.stats.ScopedFactory
import com.twitter.util.Duration
/**
* Factory to create a servo Memcache-backed Cache object. Clients are required to provide a
* serializer/deserializer for keys and values.
*/
class MemcacheFactory(memcacheClient: FinagleMemcacheClient, statsReceiver: StatsReceiver) {
private[this] val logger = Logger.get(getClass.getSimpleName)
def apply[K, V](
keySerializer: K => String,
valueSerializer: Serializer[V],
ttl: Duration
): MemcacheCache[K, V] = {
new MemcacheCache[K, V](
memcache = new ObservableMemcache(
new FinagleMemcache(memcacheClient),
new StatsReceiverCacheObserver(statsReceiver, 1000, logger)
),
ttl = ttl,
serializer = valueSerializer,
transformKey = keySerializer
)
}
}
class ScopedMemcacheFactory(memcacheClient: FinagleMemcacheClient, statsReceiver: StatsReceiver)
extends ScopedFactory[MemcacheFactory] {
override def scope(scope: RequestScope): MemcacheFactory = {
new MemcacheFactory(
memcacheClient,
statsReceiver.scope("memcache", scope.scope)
)
}
}

View File

@ -0,0 +1,24 @@
scala_library(
sources = ["*.scala"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/twitter/algebird:bijection",
"3rdparty/jvm/com/twitter/bijection:core",
"3rdparty/jvm/com/twitter/bijection:netty",
"3rdparty/jvm/com/twitter/bijection:scrooge",
"3rdparty/jvm/com/twitter/bijection:thrift",
"3rdparty/jvm/com/twitter/bijection:util",
"3rdparty/jvm/com/twitter/storehaus:core",
"finagle/finagle-stats",
"scrooge/scrooge-core/src/main/scala",
"src/scala/com/twitter/summingbird_internal/bijection:bijection-implicits",
"src/thrift/com/twitter/timelines/content_features:thrift-scala",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelines/src/main/scala/com/twitter/timelines/clients/memcache_common",
"timelines/src/main/scala/com/twitter/timelines/model/types",
"util/util-core:util-core-util",
"util/util-stats/src/main/scala/com/twitter/finagle/stats",
],
)

View File

@ -0,0 +1,39 @@
package com.twitter.timelineranker.clients.content_features_cache
import com.twitter.bijection.Injection
import com.twitter.bijection.scrooge.CompactScalaCodec
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.storehaus.Store
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelines.clients.memcache_common._
import com.twitter.timelines.content_features.{thriftscala => thrift}
import com.twitter.timelines.model.TweetId
import com.twitter.util.Duration
/**
* Content features will be stored by tweetId
*/
class ContentFeaturesMemcacheBuilder(
config: StorehausMemcacheConfig,
ttl: Duration,
statsReceiver: StatsReceiver) {
private[this] val scalaToThriftInjection: Injection[ContentFeatures, thrift.ContentFeatures] =
Injection.build[ContentFeatures, thrift.ContentFeatures](_.toThrift)(
ContentFeatures.tryFromThrift)
private[this] val thriftToBytesInjection: Injection[thrift.ContentFeatures, Array[Byte]] =
CompactScalaCodec(thrift.ContentFeatures)
private[this] implicit val valueInjection: Injection[ContentFeatures, Array[Byte]] =
scalaToThriftInjection.andThen(thriftToBytesInjection)
private[this] val underlyingBuilder =
new MemcacheStoreBuilder[TweetId, ContentFeatures](
config = config,
scopeName = "contentFeaturesCache",
statsReceiver = statsReceiver,
ttl = ttl
)
def build(): Store[TweetId, ContentFeatures] = underlyingBuilder.build()
}

View File

@ -0,0 +1,43 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/twitter/storehaus:core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"finagle/finagle-core/src/main",
"servo/util/src/main/scala",
"src/thrift/com/twitter/search:earlybird-scala",
"src/thrift/com/twitter/search/common:constants-scala",
"src/thrift/com/twitter/search/common:features-scala",
"src/thrift/com/twitter/service/metastore/gen:thrift-scala",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/contentfeatures",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/core",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/in_network_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/uteg_liked_by_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/util",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/visibility",
"timelines/src/main/scala/com/twitter/timelines/clients/gizmoduck",
"timelines/src/main/scala/com/twitter/timelines/clients/manhattan",
"timelines/src/main/scala/com/twitter/timelines/clients/relevance_search",
"timelines/src/main/scala/com/twitter/timelines/clients/tweetypie",
"timelines/src/main/scala/com/twitter/timelines/common/model",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/options",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/utils",
"timelines/src/main/scala/com/twitter/timelines/model/candidate",
"timelines/src/main/scala/com/twitter/timelines/model/tweet",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/util/bounds",
"timelines/src/main/scala/com/twitter/timelines/util/stats",
"timelines/src/main/scala/com/twitter/timelines/visibility",
"timelines/src/main/scala/com/twitter/timelines/visibility/model",
"util/util-core:util-core-util",
"util/util-core/src/main/scala/com/twitter/conversions",
"util/util-logging/src/main/scala/com/twitter/logging",
"util/util-stats/src/main/scala",
],
)

View File

@ -0,0 +1,40 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelineranker.model.CandidateTweet
import com.twitter.timelineranker.model.CandidateTweetsResult
import com.twitter.util.Future
class CandidateGenerationTransform(statsReceiver: StatsReceiver)
extends FutureArrow[HydratedCandidatesAndFeaturesEnvelope, CandidateTweetsResult] {
private[this] val numCandidateTweetsStat = statsReceiver.stat("numCandidateTweets")
private[this] val numSourceTweetsStat = statsReceiver.stat("numSourceTweets")
override def apply(
candidatesAndFeaturesEnvelope: HydratedCandidatesAndFeaturesEnvelope
): Future[CandidateTweetsResult] = {
val hydratedTweets = candidatesAndFeaturesEnvelope.candidateEnvelope.hydratedTweets.outerTweets
if (hydratedTweets.nonEmpty) {
val candidates = hydratedTweets.map { hydratedTweet =>
CandidateTweet(hydratedTweet, candidatesAndFeaturesEnvelope.features(hydratedTweet.tweetId))
}
numCandidateTweetsStat.add(candidates.size)
val sourceTweets =
candidatesAndFeaturesEnvelope.candidateEnvelope.sourceHydratedTweets.outerTweets.map {
hydratedTweet =>
CandidateTweet(
hydratedTweet,
candidatesAndFeaturesEnvelope.features(hydratedTweet.tweetId))
}
numSourceTweetsStat.add(sourceTweets.size)
Future.value(CandidateTweetsResult(candidates, sourceTweets))
} else {
Future.value(CandidateTweetsResult.Empty)
}
}
}

View File

@ -0,0 +1,112 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.servo.util.Gate
import com.twitter.storehaus.Store
import com.twitter.timelineranker.contentfeatures.ContentFeaturesProvider
import com.twitter.timelineranker.core.FutureDependencyTransformer
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelineranker.util.SearchResultUtil._
import com.twitter.timelineranker.util.CachingContentFeaturesProvider
import com.twitter.timelineranker.util.TweetHydrator
import com.twitter.timelineranker.util.TweetypieContentFeaturesProvider
import com.twitter.timelines.clients.tweetypie.TweetyPieClient
import com.twitter.timelines.model.TweetId
import com.twitter.util.Future
import com.twitter.timelines.configapi
import com.twitter.timelines.util.FutureUtils
class ContentFeaturesHydrationTransformBuilder(
tweetyPieClient: TweetyPieClient,
contentFeaturesCache: Store[TweetId, ContentFeatures],
enableContentFeaturesGate: Gate[RecapQuery],
enableTokensInContentFeaturesGate: Gate[RecapQuery],
enableTweetTextInContentFeaturesGate: Gate[RecapQuery],
enableConversationControlContentFeaturesGate: Gate[RecapQuery],
enableTweetMediaHydrationGate: Gate[RecapQuery],
hydrateInReplyToTweets: Boolean,
statsReceiver: StatsReceiver) {
val scopedStatsReceiver: StatsReceiver = statsReceiver.scope("ContentFeaturesHydrationTransform")
val tweetHydrator: TweetHydrator = new TweetHydrator(tweetyPieClient, scopedStatsReceiver)
val tweetypieContentFeaturesProvider: ContentFeaturesProvider =
new TweetypieContentFeaturesProvider(
tweetHydrator,
enableContentFeaturesGate,
enableTokensInContentFeaturesGate,
enableTweetTextInContentFeaturesGate,
enableConversationControlContentFeaturesGate,
enableTweetMediaHydrationGate,
scopedStatsReceiver
)
val cachingContentFeaturesProvider: ContentFeaturesProvider = new CachingContentFeaturesProvider(
underlying = tweetypieContentFeaturesProvider,
contentFeaturesCache = contentFeaturesCache,
statsReceiver = scopedStatsReceiver
)
val contentFeaturesProvider: configapi.FutureDependencyTransformer[RecapQuery, Seq[TweetId], Map[
TweetId,
ContentFeatures
]] = FutureDependencyTransformer.partition(
gate = enableContentFeaturesGate,
ifTrue = cachingContentFeaturesProvider,
ifFalse = tweetypieContentFeaturesProvider
)
lazy val contentFeaturesHydrationTransform: ContentFeaturesHydrationTransform =
new ContentFeaturesHydrationTransform(
contentFeaturesProvider,
enableContentFeaturesGate,
hydrateInReplyToTweets
)
def build(): ContentFeaturesHydrationTransform = contentFeaturesHydrationTransform
}
class ContentFeaturesHydrationTransform(
contentFeaturesProvider: ContentFeaturesProvider,
enableContentFeaturesGate: Gate[RecapQuery],
hydrateInReplyToTweets: Boolean)
extends FutureArrow[
HydratedCandidatesAndFeaturesEnvelope,
HydratedCandidatesAndFeaturesEnvelope
] {
override def apply(
request: HydratedCandidatesAndFeaturesEnvelope
): Future[HydratedCandidatesAndFeaturesEnvelope] = {
if (enableContentFeaturesGate(request.candidateEnvelope.query)) {
val searchResults = request.candidateEnvelope.searchResults
val sourceTweetIdMap = searchResults.map { searchResult =>
(searchResult.id, getRetweetSourceTweetId(searchResult).getOrElse(searchResult.id))
}.toMap
val inReplyToTweetIds = if (hydrateInReplyToTweets) {
searchResults.flatMap(getInReplyToTweetId)
} else {
Seq.empty
}
val tweetIdsToHydrate = (sourceTweetIdMap.values ++ inReplyToTweetIds).toSeq.distinct
val contentFeaturesMapFuture = if (tweetIdsToHydrate.nonEmpty) {
contentFeaturesProvider(request.candidateEnvelope.query, tweetIdsToHydrate)
} else {
FutureUtils.EmptyMap[TweetId, ContentFeatures]
}
Future.value(
request.copy(
contentFeaturesFuture = contentFeaturesMapFuture,
tweetSourceTweetMap = sourceTweetIdMap,
inReplyToTweetIds = inReplyToTweetIds.toSet
)
)
} else {
Future.value(request)
}
}
}

View File

@ -0,0 +1,15 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.util.Future
/**
* Create a CandidateEnvelope based on the incoming RecapQuery
*/
object CreateCandidateEnvelopeTransform extends FutureArrow[RecapQuery, CandidateEnvelope] {
override def apply(query: RecapQuery): Future[CandidateEnvelope] = {
Future.value(CandidateEnvelope(query))
}
}

View File

@ -0,0 +1,33 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.util.Future
/**
* Fetches all data required for feature hydration and generates the HydratedCandidatesAndFeaturesEnvelope
* @param tweetHydrationAndFilteringPipeline Pipeline which fetches the candidate tweets, hydrates and filters them
* @param languagesService Fetch user languages, required for feature hydration
* @param userProfileInfoService Fetch user profile info, required for feature hydration
*/
class FeatureHydrationDataTransform(
tweetHydrationAndFilteringPipeline: FutureArrow[RecapQuery, CandidateEnvelope],
languagesService: UserLanguagesTransform,
userProfileInfoService: UserProfileInfoTransform)
extends FutureArrow[RecapQuery, HydratedCandidatesAndFeaturesEnvelope] {
override def apply(request: RecapQuery): Future[HydratedCandidatesAndFeaturesEnvelope] = {
Future
.join(
languagesService(request),
userProfileInfoService(request),
tweetHydrationAndFilteringPipeline(request)).map {
case (languages, userProfileInfo, transformedCandidateEnvelope) =>
HydratedCandidatesAndFeaturesEnvelope(
transformedCandidateEnvelope,
languages,
userProfileInfo)
}
}
}

View File

@ -0,0 +1,198 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.Stat
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.parameters.recap.RecapQueryContext
import com.twitter.timelineranker.parameters.in_network_tweets.InNetworkTweetParams.RecycledMaxFollowedUsersEnableAntiDilutionParam
import com.twitter.timelineranker.visibility.FollowGraphDataProvider
import com.twitter.timelines.earlybird.common.options.AuthorScoreAdjustments
import com.twitter.util.Future
/**
* Transform which conditionally augments follow graph data using the real graph,
* derived from the earlybirdOptions passed in the query
*
* @param followGraphDataProvider data provider to be used for fetching updated mutual follow info
* @param maxFollowedUsersProvider max number of users to return
* @param enableRealGraphUsersProvider should we augment using real graph data?
* @param maxRealGraphAndFollowedUsersProvider max combined users to return, overrides maxFollowedUsersProvider above
* @param statsReceiver scoped stats received
*/
class FollowAndRealGraphCombiningTransform(
followGraphDataProvider: FollowGraphDataProvider,
maxFollowedUsersProvider: DependencyProvider[Int],
enableRealGraphUsersProvider: DependencyProvider[Boolean],
maxRealGraphAndFollowedUsersProvider: DependencyProvider[Int],
imputeRealGraphAuthorWeightsProvider: DependencyProvider[Boolean],
imputeRealGraphAuthorWeightsPercentileProvider: DependencyProvider[Int],
statsReceiver: StatsReceiver)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
// Number of authors in the seedset after mixing followed users and real graph users
// Only have this stat if user follows >= maxFollowedUsers and enableRealGraphUsers is true and onlyRealGraphUsers is false
val numFollowAndRealGraphUsersStat: Stat = statsReceiver.stat("numFollowAndRealGraphUsers")
val numFollowAndRealGraphUsersFromFollowGraphStat =
statsReceiver.scope("numFollowAndRealGraphUsers").stat("FollowGraphUsers")
val numFollowAndRealGraphUsersFromRealGraphStat =
statsReceiver.scope("numFollowAndRealGraphUsers").stat("RealGraphUsers")
val numFollowAndRealGraphUsersFromRealGraphCounter =
statsReceiver.scope("numFollowAndRealGraphUsers").counter()
// Number of authors in the seedset with only followed users
// Only have this stat if user follows >= maxFollowedUsers and enableRealGraphUsers is false
val numFollowedUsersStat: Stat = statsReceiver.stat("numFollowedUsers")
// Number of authors in the seedset with only followed users
// Only have this stat if user follows < maxFollowedUsers
val numFollowedUsersLessThanMaxStat: Stat = statsReceiver.stat("numFollowedUsersLessThanMax")
val numFollowedUsersLessThanMaxCounter =
statsReceiver.scope("numFollowedUsersLessThanMax").counter()
val numFollowedUsersMoreThanMaxStat: Stat = statsReceiver.stat("numFollowedUsersMoreThanMax")
val numFollowedUsersMoreThanMaxCounter =
statsReceiver.scope("numFollowedUsersMoreThanMax").counter()
val realGraphAuthorWeightsSumProdStat: Stat = statsReceiver.stat("realGraphAuthorWeightsSumProd")
val realGraphAuthorWeightsSumMinExpStat: Stat =
statsReceiver.stat("realGraphAuthorWeightsSumMinExp")
val realGraphAuthorWeightsSumP50ExpStat: Stat =
statsReceiver.stat("realGraphAuthorWeightsSumP50Exp")
val realGraphAuthorWeightsSumP95ExpStat: Stat =
statsReceiver.stat("realGraphAuthorWeightsSumP95Exp")
val numAuthorsWithoutRealgraphScoreStat: Stat =
statsReceiver.stat("numAuthorsWithoutRealgraphScore")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val realGraphData = envelope.query.earlybirdOptions
.map(_.authorScoreAdjustments.authorScoreMap)
.getOrElse(Map.empty)
Future
.join(
envelope.followGraphData.followedUserIdsFuture,
envelope.followGraphData.mutedUserIdsFuture
).map {
case (followedUserIds, mutedUserIds) =>
// Anti-dilution param for DDG-16198
val recycledMaxFollowedUsersEnableAntiDilutionParamProvider =
DependencyProvider.from(RecycledMaxFollowedUsersEnableAntiDilutionParam)
val maxFollowedUsers = {
if (followedUserIds.size > RecapQueryContext.MaxFollowedUsers.default && recycledMaxFollowedUsersEnableAntiDilutionParamProvider(
envelope.query)) {
// trigger experiment
maxFollowedUsersProvider(envelope.query)
} else {
maxFollowedUsersProvider(envelope.query)
}
}
val filteredRealGraphUserIds = realGraphData.keySet
.filterNot(mutedUserIds)
.take(maxFollowedUsers)
.toSeq
val filteredFollowedUserIds = followedUserIds.filterNot(mutedUserIds)
if (followedUserIds.size < maxFollowedUsers) {
numFollowedUsersLessThanMaxStat.add(filteredFollowedUserIds.size)
// stats
numFollowedUsersLessThanMaxCounter.incr()
(filteredFollowedUserIds, false)
} else {
numFollowedUsersMoreThanMaxStat.add(filteredFollowedUserIds.size)
numFollowedUsersMoreThanMaxCounter.incr()
if (enableRealGraphUsersProvider(envelope.query)) {
val maxRealGraphAndFollowedUsersNum =
maxRealGraphAndFollowedUsersProvider(envelope.query)
require(
maxRealGraphAndFollowedUsersNum >= maxFollowedUsers,
"maxRealGraphAndFollowedUsers must be greater than or equal to maxFollowedUsers."
)
val recentFollowedUsersNum = RecapQueryContext.MaxFollowedUsers.bounds
.apply(maxRealGraphAndFollowedUsersNum - filteredRealGraphUserIds.size)
val recentFollowedUsers =
filteredFollowedUserIds
.filterNot(filteredRealGraphUserIds.contains)
.take(recentFollowedUsersNum)
val filteredFollowAndRealGraphUserIds =
recentFollowedUsers ++ filteredRealGraphUserIds
// Track the size of recentFollowedUsers from SGS
numFollowAndRealGraphUsersFromFollowGraphStat.add(recentFollowedUsers.size)
// Track the size of filteredRealGraphUserIds from real graph dataset.
numFollowAndRealGraphUsersFromRealGraphStat.add(filteredRealGraphUserIds.size)
numFollowAndRealGraphUsersFromRealGraphCounter.incr()
numFollowAndRealGraphUsersStat.add(filteredFollowAndRealGraphUserIds.size)
(filteredFollowAndRealGraphUserIds, true)
} else {
numFollowedUsersStat.add(followedUserIds.size)
(filteredFollowedUserIds, false)
}
}
}.map {
case (updatedFollowSeq, shouldUpdateMutualFollows) =>
val updatedMutualFollowing = if (shouldUpdateMutualFollows) {
followGraphDataProvider.getMutuallyFollowingUserIds(
envelope.query.userId,
updatedFollowSeq)
} else {
envelope.followGraphData.mutuallyFollowingUserIdsFuture
}
val followGraphData = envelope.followGraphData.copy(
followedUserIdsFuture = Future.value(updatedFollowSeq),
mutuallyFollowingUserIdsFuture = updatedMutualFollowing
)
val authorIdsWithRealgraphScore = realGraphData.keySet
val authorIdsWithoutRealgraphScores =
updatedFollowSeq.filterNot(authorIdsWithRealgraphScore.contains)
//stat for logging the percentage of users' followings that do not have a realgraph score
if (updatedFollowSeq.nonEmpty)
numAuthorsWithoutRealgraphScoreStat.add(
authorIdsWithoutRealgraphScores.size / updatedFollowSeq.size * 100)
if (imputeRealGraphAuthorWeightsProvider(envelope.query) && realGraphData.nonEmpty) {
val imputedScorePercentile =
imputeRealGraphAuthorWeightsPercentileProvider(envelope.query) / 100.0
val existingAuthorIdScores = realGraphData.values.toList.sorted
val imputedScoreIndex = Math.min(
existingAuthorIdScores.length - 1,
(existingAuthorIdScores.length * imputedScorePercentile).toInt)
val imputedScore = existingAuthorIdScores(imputedScoreIndex)
val updatedAuthorScoreMap = realGraphData ++ authorIdsWithoutRealgraphScores
.map(_ -> imputedScore).toMap
imputedScorePercentile match {
case 0.0 =>
realGraphAuthorWeightsSumMinExpStat.add(updatedAuthorScoreMap.values.sum.toFloat)
case 0.5 =>
realGraphAuthorWeightsSumP50ExpStat.add(updatedAuthorScoreMap.values.sum.toFloat)
case 0.95 =>
realGraphAuthorWeightsSumP95ExpStat.add(updatedAuthorScoreMap.values.sum.toFloat)
case _ =>
}
val earlybirdOptionsWithUpdatedAuthorScoreMap = envelope.query.earlybirdOptions
.map(_.copy(authorScoreAdjustments = AuthorScoreAdjustments(updatedAuthorScoreMap)))
val updatedQuery =
envelope.query.copy(earlybirdOptions = earlybirdOptionsWithUpdatedAuthorScoreMap)
envelope.copy(query = updatedQuery, followGraphData = followGraphData)
} else {
envelope.query.earlybirdOptions
.map(_.authorScoreAdjustments.authorScoreMap.values.sum.toFloat).foreach {
realGraphAuthorWeightsSumProdStat.add(_)
}
envelope.copy(followGraphData = followGraphData)
}
}
}
}

View File

@ -0,0 +1,23 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.visibility.FollowGraphDataProvider
import com.twitter.util.Future
class FollowGraphDataTransform(
followGraphDataProvider: FollowGraphDataProvider,
maxFollowedUsersProvider: DependencyProvider[Int])
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val followGraphData = followGraphDataProvider.getAsync(
envelope.query.userId,
maxFollowedUsersProvider(envelope.query)
)
Future.value(envelope.copy(followGraphData = followGraphData))
}
}

View File

@ -0,0 +1,31 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.util.Future
/**
* Transform that explicitly hydrates candidate tweets and fetches source tweets in parallel
* and then joins the results back into the original Envelope
* @param candidateTweetHydration Pipeline that hydrates candidate tweets
* @param sourceTweetHydration Pipeline that fetches and hydrates source tweets
*/
class HydrateTweetsAndSourceTweetsInParallelTransform(
candidateTweetHydration: FutureArrow[CandidateEnvelope, CandidateEnvelope],
sourceTweetHydration: FutureArrow[CandidateEnvelope, CandidateEnvelope])
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
Future
.join(
candidateTweetHydration(envelope),
sourceTweetHydration(envelope)
).map {
case (candidateTweetEnvelope, sourceTweetEnvelope) =>
envelope.copy(
hydratedTweets = candidateTweetEnvelope.hydratedTweets,
sourceSearchResults = sourceTweetEnvelope.sourceSearchResults,
sourceHydratedTweets = sourceTweetEnvelope.sourceHydratedTweets
)
}
}
}

View File

@ -0,0 +1,96 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.logging.Logger
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.core.HydratedTweets
import com.twitter.timelineranker.util.TweetFilters
import com.twitter.timelineranker.util.TweetsPostFilter
import com.twitter.timelines.model.UserId
import com.twitter.util.Future
object HydratedTweetsFilterTransform {
val EmptyFollowGraphDataTuple: (Seq[UserId], Seq[UserId], Set[UserId]) =
(Seq.empty[UserId], Seq.empty[UserId], Set.empty[UserId])
val DefaultNumRetweetsAllowed = 1
// Number of duplicate retweets (including the first one) allowed.
// For example,
// If there are 7 retweets of a given tweet, the following value will cause 5 of them
// to be returned after filtering and the additional 2 will be filtered out.
val NumDuplicateRetweetsAllowed = 5
}
/**
* Transform which takes TweetFilters ValueSets for inner and outer tweets and uses
* TweetsPostFilter to filter down the HydratedTweets using the supplied filters
*
* @param useFollowGraphData - use follow graph for filtering; otherwise only does filtering
* independent of follow graph data
* @param useSourceTweets - only needed when filtering extended replies
* @param statsReceiver - scoped stats receiver
*/
class HydratedTweetsFilterTransform(
outerFilters: TweetFilters.ValueSet,
innerFilters: TweetFilters.ValueSet,
useFollowGraphData: Boolean,
useSourceTweets: Boolean,
statsReceiver: StatsReceiver,
numRetweetsAllowed: Int = HydratedTweetsFilterTransform.DefaultNumRetweetsAllowed)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
import HydratedTweetsFilterTransform._
val logger: Logger = Logger.get(getClass.getSimpleName)
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
if (outerFilters == TweetFilters.None) {
Future.value(envelope)
} else {
val tweetsPostOuterFilter = new TweetsPostFilter(outerFilters, logger, statsReceiver)
val tweetsPostInnerFilter = new TweetsPostFilter(innerFilters, logger, statsReceiver)
val graphData = if (useFollowGraphData) {
Future.join(
envelope.followGraphData.followedUserIdsFuture,
envelope.followGraphData.inNetworkUserIdsFuture,
envelope.followGraphData.mutedUserIdsFuture
)
} else {
Future.value(EmptyFollowGraphDataTuple)
}
val sourceTweets = if (useSourceTweets) {
envelope.sourceHydratedTweets.outerTweets
} else {
Nil
}
graphData.map {
case (followedUserIds, inNetworkUserIds, mutedUserIds) =>
val outerTweets = tweetsPostOuterFilter(
userId = envelope.query.userId,
followedUserIds = followedUserIds,
inNetworkUserIds = inNetworkUserIds,
mutedUserIds = mutedUserIds,
tweets = envelope.hydratedTweets.outerTweets,
numRetweetsAllowed = numRetweetsAllowed,
sourceTweets = sourceTweets
)
val innerTweets = tweetsPostInnerFilter(
userId = envelope.query.userId,
followedUserIds = followedUserIds,
inNetworkUserIds = inNetworkUserIds,
mutedUserIds = mutedUserIds,
// inner tweets refers to quoted tweets not source tweets, and special rulesets
// in birdherd handle visibility of viewer to inner tweet author for these tweets.
tweets = envelope.hydratedTweets.innerTweets,
numRetweetsAllowed = numRetweetsAllowed,
sourceTweets = sourceTweets
)
envelope.copy(hydratedTweets = HydratedTweets(outerTweets, innerTweets))
}
}
}
}

View File

@ -0,0 +1,38 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelines.earlybird.common.utils.EarlybirdFeaturesHydrator
import com.twitter.util.Future
object InNetworkTweetsSearchFeaturesHydrationTransform
extends FutureArrow[
HydratedCandidatesAndFeaturesEnvelope,
HydratedCandidatesAndFeaturesEnvelope
] {
override def apply(
request: HydratedCandidatesAndFeaturesEnvelope
): Future[HydratedCandidatesAndFeaturesEnvelope] = {
Future
.join(
request.candidateEnvelope.followGraphData.followedUserIdsFuture,
request.candidateEnvelope.followGraphData.mutuallyFollowingUserIdsFuture
).map {
case (followedIds, mutuallyFollowingIds) =>
val featuresByTweetId = EarlybirdFeaturesHydrator.hydrate(
searcherUserId = request.candidateEnvelope.query.userId,
searcherProfileInfo = request.userProfileInfo,
followedUserIds = followedIds,
mutuallyFollowingUserIds = mutuallyFollowingIds,
userLanguages = request.userLanguages,
uiLanguageCode = request.candidateEnvelope.query.deviceContext.flatMap(_.languageCode),
searchResults = request.candidateEnvelope.searchResults,
sourceTweetSearchResults = request.candidateEnvelope.sourceSearchResults,
tweets = request.candidateEnvelope.hydratedTweets.outerTweets,
sourceTweets = request.candidateEnvelope.sourceHydratedTweets.outerTweets
)
request.copy(features = featuresByTweetId)
}
}
}

View File

@ -0,0 +1,55 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.CandidateTweet
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.util.Future
import com.twitter.util.Time
import scala.util.Random
/**
* picks up one or more random tweets and sets its tweetFeatures.isRandomTweet field to true.
*/
class MarkRandomTweetTransform(
includeRandomTweetProvider: DependencyProvider[Boolean],
randomGenerator: Random = new Random(Time.now.inMilliseconds),
includeSingleRandomTweetProvider: DependencyProvider[Boolean],
probabilityRandomTweetProvider: DependencyProvider[Double])
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val includeRandomTweet = includeRandomTweetProvider(envelope.query)
val includeSingleRandomTweet = includeSingleRandomTweetProvider(envelope.query)
val probabilityRandomTweet = probabilityRandomTweetProvider(envelope.query)
val searchResults = envelope.searchResults
if (!includeRandomTweet || searchResults.isEmpty) { // random tweet off
Future.value(envelope)
} else if (includeSingleRandomTweet) { // pick only one
val randomIdx = randomGenerator.nextInt(searchResults.size)
val randomTweet = searchResults(randomIdx)
val randomTweetWithFlag = randomTweet.copy(
tweetFeatures = randomTweet.tweetFeatures
.orElse(Some(CandidateTweet.DefaultFeatures))
.map(_.copy(isRandomTweet = Some(true)))
)
val updatedSearchResults = searchResults.updated(randomIdx, randomTweetWithFlag)
Future.value(envelope.copy(searchResults = updatedSearchResults))
} else { // pick tweets with perTweetProbability
val updatedSearchResults = searchResults.map { result =>
if (randomGenerator.nextDouble() < probabilityRandomTweet) {
result.copy(
tweetFeatures = result.tweetFeatures
.orElse(Some(CandidateTweet.DefaultFeatures))
.map(_.copy(isRandomTweet = Some(true))))
} else
result
}
Future.value(envelope.copy(searchResults = updatedSearchResults))
}
}
}

View File

@ -0,0 +1,49 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.util.Future
object OutOfNetworkRepliesToUserIdSearchResultsTransform {
val DefaultMaxTweetCount = 100
}
// Requests search results for out-of-network replies to a user Id
class OutOfNetworkRepliesToUserIdSearchResultsTransform(
searchClient: SearchClient,
statsReceiver: StatsReceiver,
logSearchDebugInfo: Boolean = true)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
private[this] val maxCountStat = statsReceiver.stat("maxCount")
private[this] val numResultsFromSearchStat = statsReceiver.stat("numResultsFromSearch")
private[this] val earlybirdScoreX100Stat = statsReceiver.stat("earlybirdScoreX100")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val maxCount = envelope.query.maxCount
.getOrElse(OutOfNetworkRepliesToUserIdSearchResultsTransform.DefaultMaxTweetCount)
maxCountStat.add(maxCount)
envelope.followGraphData.followedUserIdsFuture
.flatMap {
case followedIds =>
searchClient
.getOutOfNetworkRepliesToUserId(
userId = envelope.query.userId,
followedUserIds = followedIds.toSet,
maxCount = maxCount,
earlybirdOptions = envelope.query.earlybirdOptions,
logSearchDebugInfo
).map { results =>
numResultsFromSearchStat.add(results.size)
results.foreach { result =>
val earlybirdScoreX100 =
result.metadata.flatMap(_.score).getOrElse(0.0).toFloat * 100
earlybirdScoreX100Stat.add(earlybirdScoreX100)
}
envelope.copy(searchResults = results)
}
}
}
}

View File

@ -0,0 +1,31 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelines.earlybird.common.utils.EarlybirdFeaturesHydrator
import com.twitter.util.Future
object OutOfNetworkTweetsSearchFeaturesHydrationTransform
extends FutureArrow[
HydratedCandidatesAndFeaturesEnvelope,
HydratedCandidatesAndFeaturesEnvelope
] {
override def apply(
request: HydratedCandidatesAndFeaturesEnvelope
): Future[HydratedCandidatesAndFeaturesEnvelope] = {
val featuresByTweetId = EarlybirdFeaturesHydrator.hydrate(
searcherUserId = request.candidateEnvelope.query.userId,
searcherProfileInfo = request.userProfileInfo,
followedUserIds = Seq.empty,
mutuallyFollowingUserIds = Set.empty,
userLanguages = request.userLanguages,
uiLanguageCode = request.candidateEnvelope.query.deviceContext.flatMap(_.languageCode),
searchResults = request.candidateEnvelope.searchResults,
sourceTweetSearchResults = Seq.empty,
tweets = request.candidateEnvelope.hydratedTweets.outerTweets,
sourceTweets = Seq.empty
)
Future.value(request.copy(features = featuresByTweetId))
}
}

View File

@ -0,0 +1,29 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.model.TweetId
import com.twitter.util.Future
trait RecapHydrationSearchResultsTransformBase
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
protected def statsReceiver: StatsReceiver
protected def searchClient: SearchClient
private[this] val numResultsFromSearchStat = statsReceiver.stat("numResultsFromSearch")
def tweetIdsToHydrate(envelope: CandidateEnvelope): Seq[TweetId]
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
searchClient
.getTweetsScoredForRecap(
envelope.query.userId,
tweetIdsToHydrate(envelope),
envelope.query.earlybirdOptions
).map { results =>
numResultsFromSearchStat.add(results.size)
envelope.copy(searchResults = results)
}
}
}

View File

@ -0,0 +1,89 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.model.TweetIdRange
import com.twitter.timelineranker.parameters.recap.RecapParams
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.clients.relevance_search.SearchClient.TweetTypes
import com.twitter.util.Future
/**
* Fetch recap/recycled search results using the search client
* and populate them into the CandidateEnvelope
*/
class RecapSearchResultsTransform(
searchClient: SearchClient,
maxCountProvider: DependencyProvider[Int],
returnAllResultsProvider: DependencyProvider[Boolean],
relevanceOptionsMaxHitsToProcessProvider: DependencyProvider[Int],
enableExcludeSourceTweetIdsProvider: DependencyProvider[Boolean],
enableSettingTweetTypesWithTweetKindOptionProvider: DependencyProvider[Boolean],
perRequestSearchClientIdProvider: DependencyProvider[Option[String]],
relevanceSearchProvider: DependencyProvider[Boolean] =
DependencyProvider.from(RecapParams.EnableRelevanceSearchParam),
statsReceiver: StatsReceiver,
logSearchDebugInfo: Boolean = true)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
private[this] val maxCountStat = statsReceiver.stat("maxCount")
private[this] val numResultsFromSearchStat = statsReceiver.stat("numResultsFromSearch")
private[this] val excludedTweetIdsStat = statsReceiver.stat("excludedTweetIds")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val maxCount = maxCountProvider(envelope.query)
maxCountStat.add(maxCount)
val excludedTweetIdsOpt = envelope.query.excludedTweetIds
excludedTweetIdsOpt.foreach { excludedTweetIds =>
excludedTweetIdsStat.add(excludedTweetIds.size)
}
val tweetIdRange = envelope.query.range
.map(TweetIdRange.fromTimelineRange)
.getOrElse(TweetIdRange.default)
val beforeTweetIdExclusive = tweetIdRange.toId
val afterTweetIdExclusive = tweetIdRange.fromId
val returnAllResults = returnAllResultsProvider(envelope.query)
val relevanceOptionsMaxHitsToProcess = relevanceOptionsMaxHitsToProcessProvider(envelope.query)
Future
.join(
envelope.followGraphData.followedUserIdsFuture,
envelope.followGraphData.retweetsMutedUserIdsFuture
).flatMap {
case (followedIds, retweetsMutedIds) =>
val followedIdsIncludingSelf = followedIds.toSet + envelope.query.userId
searchClient
.getUsersTweetsForRecap(
userId = envelope.query.userId,
followedUserIds = followedIdsIncludingSelf,
retweetsMutedUserIds = retweetsMutedIds,
maxCount = maxCount,
tweetTypes = TweetTypes.fromTweetKindOption(envelope.query.options),
searchOperator = envelope.query.searchOperator,
beforeTweetIdExclusive = beforeTweetIdExclusive,
afterTweetIdExclusive = afterTweetIdExclusive,
enableSettingTweetTypesWithTweetKindOption =
enableSettingTweetTypesWithTweetKindOptionProvider(envelope.query),
excludedTweetIds = excludedTweetIdsOpt,
earlybirdOptions = envelope.query.earlybirdOptions,
getOnlyProtectedTweets = false,
logSearchDebugInfo = logSearchDebugInfo,
returnAllResults = returnAllResults,
enableExcludeSourceTweetIdsQuery =
enableExcludeSourceTweetIdsProvider(envelope.query),
relevanceSearch = relevanceSearchProvider(envelope.query),
searchClientId = perRequestSearchClientIdProvider(envelope.query),
relevanceOptionsMaxHitsToProcess = relevanceOptionsMaxHitsToProcess
).map { results =>
numResultsFromSearchStat.add(results.size)
envelope.copy(searchResults = results)
}
}
}
}

View File

@ -0,0 +1,67 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.util.SearchResultUtil
import com.twitter.util.Future
/**
* Truncate the search results by score. Assumes that the search results are sorted in
* score-descending order unless extraSortBeforeTruncation is set to true.
*
* This transform has two main use cases:
*
* - when returnAllResults is set to true, earlybird returns (numResultsPerShard * number of shards)
* results. this transform is then used to further truncate the result, so that the size will be the
* same as when returnAllResults is set to false.
*
* - we retrieve extra number of results from earlybird, as specified in MaxCountMultiplierParam,
* so that we are left with sufficient number of candidates after hydration and filtering.
* this transform will be used to get rid of extra results we ended up not using.
*/
class RecapSearchResultsTruncationTransform(
extraSortBeforeTruncationGate: DependencyProvider[Boolean],
maxCountProvider: DependencyProvider[Int],
statsReceiver: StatsReceiver)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
private[this] val postTruncationSizeStat = statsReceiver.stat("postTruncationSize")
private[this] val earlybirdScoreX100Stat = statsReceiver.stat("earlybirdScoreX100")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val sortBeforeTruncation = extraSortBeforeTruncationGate(envelope.query)
val maxCount = maxCountProvider(envelope.query)
val searchResults = envelope.searchResults
// set aside results that are marked by isRandomTweet field
val (randomTweetSeq, searchResultsExcludingRandom) = searchResults.partition { result =>
result.tweetFeatures.flatMap(_.isRandomTweet).getOrElse(false)
}
// sort and truncate searchResults other than the random tweet
val maxCountExcludingRandom = Math.max(0, maxCount - randomTweetSeq.size)
val truncatedResultsExcludingRandom =
if (sortBeforeTruncation || searchResultsExcludingRandom.size > maxCountExcludingRandom) {
val sorted = if (sortBeforeTruncation) {
searchResultsExcludingRandom.sortWith(
SearchResultUtil.getScore(_) > SearchResultUtil.getScore(_))
} else searchResultsExcludingRandom
sorted.take(maxCountExcludingRandom)
} else searchResultsExcludingRandom
// put back the random tweet set aside previously
val allTruncatedResults = truncatedResultsExcludingRandom ++ randomTweetSeq
// stats
postTruncationSizeStat.add(allTruncatedResults.size)
allTruncatedResults.foreach { result =>
val earlybirdScoreX100 =
result.metadata.flatMap(_.score).getOrElse(0.0).toFloat * 100
earlybirdScoreX100Stat.add(earlybirdScoreX100)
}
Future.value(envelope.copy(searchResults = allTruncatedResults))
}
}

View File

@ -0,0 +1,23 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelines.model.TweetId
import com.twitter.util.Future
import scala.collection.mutable
/**
* Remove duplicate search results and order them reverse-chron.
*/
object SearchResultDedupAndSortingTransform
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val seenTweetIds = mutable.Set.empty[TweetId]
val dedupedResults = envelope.searchResults
.filter(result => seenTweetIds.add(result.id))
.sortBy(_.id)(Ordering[TweetId].reverse)
val transformedEnvelope = envelope.copy(searchResults = dedupedResults)
Future.value(transformedEnvelope)
}
}

View File

@ -0,0 +1,62 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.util.SourceTweetsUtil
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.util.Future
object SourceTweetsSearchResultsTransform {
val EmptySearchResults: Seq[ThriftSearchResult] = Seq.empty[ThriftSearchResult]
val EmptySearchResultsFuture: Future[Seq[ThriftSearchResult]] = Future.value(EmptySearchResults)
}
/**
* Fetch source tweets for a given set of search results
* Collects ids of source tweets, including extended reply and reply source tweets if needed,
* fetches those tweets from search and populates them into the envelope
*/
class SourceTweetsSearchResultsTransform(
searchClient: SearchClient,
failOpenHandler: FailOpenHandler,
hydrateReplyRootTweetProvider: DependencyProvider[Boolean],
perRequestSourceSearchClientIdProvider: DependencyProvider[Option[String]],
statsReceiver: StatsReceiver)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
import SourceTweetsSearchResultsTransform._
private val scopedStatsReceiver = statsReceiver.scope(getClass.getSimpleName)
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
failOpenHandler {
envelope.followGraphData.followedUserIdsFuture.flatMap { followedUserIds =>
// NOTE: tweetIds are pre-computed as a performance optimisation.
val searchResultsTweetIds = envelope.searchResults.map(_.id).toSet
val sourceTweetIds = SourceTweetsUtil.getSourceTweetIds(
searchResults = envelope.searchResults,
searchResultsTweetIds = searchResultsTweetIds,
followedUserIds = followedUserIds,
shouldIncludeReplyRootTweets = hydrateReplyRootTweetProvider(envelope.query),
statsReceiver = scopedStatsReceiver
)
if (sourceTweetIds.isEmpty) {
EmptySearchResultsFuture
} else {
searchClient.getTweetsScoredForRecap(
userId = envelope.query.userId,
tweetIds = sourceTweetIds,
earlybirdOptions = envelope.query.earlybirdOptions,
logSearchDebugInfo = false,
searchClientId = perRequestSourceSearchClientIdProvider(envelope.query)
)
}
}
} { _: Throwable => EmptySearchResultsFuture }.map { sourceSearchResults =>
envelope.copy(sourceSearchResults = sourceSearchResults)
}
}
}

View File

@ -0,0 +1,37 @@
package com.twitter.timelineranker.common
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelines.model.tweet.HydratedTweet
import com.twitter.util.Future
/**
* trims searchResults to match with hydratedTweets
* (if we previously filtered out hydrated tweets, this transform filters the search result set
* down to match the hydrated tweets.)
*/
object TrimToMatchHydratedTweetsTransform
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val filteredSearchResults =
trimSearchResults(envelope.searchResults, envelope.hydratedTweets.outerTweets)
val filteredSourceSearchResults =
trimSearchResults(envelope.sourceSearchResults, envelope.sourceHydratedTweets.outerTweets)
Future.value(
envelope.copy(
searchResults = filteredSearchResults,
sourceSearchResults = filteredSourceSearchResults
)
)
}
private def trimSearchResults(
searchResults: Seq[ThriftSearchResult],
hydratedTweets: Seq[HydratedTweet]
): Seq[ThriftSearchResult] = {
val filteredTweetIds = hydratedTweets.map(_.tweetId).toSet
searchResults.filter(result => filteredTweetIds.contains(result.id))
}
}

View File

@ -0,0 +1,57 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.util.SourceTweetsUtil
import com.twitter.util.Future
/**
* trims elements of the envelope other than the searchResults
* (i.e. sourceSearchResults, hydratedTweets, sourceHydratedTweets) to match with searchResults.
*/
class TrimToMatchSearchResultsTransform(
hydrateReplyRootTweetProvider: DependencyProvider[Boolean],
statsReceiver: StatsReceiver)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
private val scopedStatsReceiver = statsReceiver.scope(getClass.getSimpleName)
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val searchResults = envelope.searchResults
val searchResultsIds = searchResults.map(_.id).toSet
// Trim rest of the seqs to match top search results.
val hydratedTweets = envelope.hydratedTweets.outerTweets
val topHydratedTweets = hydratedTweets.filter(ht => searchResultsIds.contains(ht.tweetId))
envelope.followGraphData.followedUserIdsFuture.map { followedUserIds =>
val sourceTweetIdsOfTopResults =
SourceTweetsUtil
.getSourceTweetIds(
searchResults = searchResults,
searchResultsTweetIds = searchResultsIds,
followedUserIds = followedUserIds,
shouldIncludeReplyRootTweets = hydrateReplyRootTweetProvider(envelope.query),
statsReceiver = scopedStatsReceiver
).toSet
val sourceTweetSearchResultsForTopN =
envelope.sourceSearchResults.filter(r => sourceTweetIdsOfTopResults.contains(r.id))
val hydratedSourceTweetsForTopN =
envelope.sourceHydratedTweets.outerTweets.filter(ht =>
sourceTweetIdsOfTopResults.contains(ht.tweetId))
val hydratedTweetsForEnvelope = envelope.hydratedTweets.copy(outerTweets = topHydratedTweets)
val hydratedSourceTweetsForEnvelope =
envelope.sourceHydratedTweets.copy(outerTweets = hydratedSourceTweetsForTopN)
envelope.copy(
hydratedTweets = hydratedTweetsForEnvelope,
searchResults = searchResults,
sourceHydratedTweets = hydratedSourceTweetsForEnvelope,
sourceSearchResults = sourceTweetSearchResultsForTopN
)
}
}
}

View File

@ -0,0 +1,62 @@
package com.twitter.timelineranker.common
import com.twitter.conversions.DurationOps._
import com.twitter.finagle.IndividualRequestTimeoutException
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.core.HydratedTweets
import com.twitter.timelineranker.model.PartiallyHydratedTweet
import com.twitter.timelines.model.tweet.HydratedTweet
import com.twitter.util.Future
object TweetHydrationTransform {
val EmptyHydratedTweets: HydratedTweets =
HydratedTweets(Seq.empty[HydratedTweet], Seq.empty[HydratedTweet])
val EmptyHydratedTweetsFuture: Future[HydratedTweets] = Future.value(EmptyHydratedTweets)
}
object CandidateTweetHydrationTransform extends TweetHydrationTransform {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
hydrate(
searchResults = envelope.searchResults,
envelope = envelope
).map { tweets => envelope.copy(hydratedTweets = tweets) }
}
}
object SourceTweetHydrationTransform extends TweetHydrationTransform {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
hydrate(
searchResults = envelope.sourceSearchResults,
envelope = envelope
).map { tweets => envelope.copy(sourceHydratedTweets = tweets) }
}
}
// Static IRTE to indicate timeout in tweet hydrator. Placeholder timeout duration of 0 millis is used
// since we are only concerned with the source of the exception.
object TweetHydrationTimeoutException extends IndividualRequestTimeoutException(0.millis) {
serviceName = "tweetHydrator"
}
/**
* Transform which hydrates tweets in the CandidateEnvelope
**/
trait TweetHydrationTransform extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
import TweetHydrationTransform._
protected def hydrate(
searchResults: Seq[ThriftSearchResult],
envelope: CandidateEnvelope
): Future[HydratedTweets] = {
if (searchResults.nonEmpty) {
Future.value(
HydratedTweets(searchResults.map(PartiallyHydratedTweet.fromSearchResult))
)
} else {
EmptyHydratedTweetsFuture
}
}
}

View File

@ -0,0 +1,85 @@
package com.twitter.timelineranker.common
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.servo.util.Gate
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelineranker.parameters.recap.RecapParams
import com.twitter.timelineranker.parameters.uteg_liked_by_tweets.UtegLikedByTweetsParams
import com.twitter.timelineranker.util.TweetFilters
import com.twitter.timelines.common.model.TweetKindOption
import com.twitter.util.Future
import scala.collection.mutable
object TweetKindOptionHydratedTweetsFilterTransform {
private[common] val enableExpandedExtendedRepliesGate: Gate[RecapQuery] =
RecapQuery.paramGate(RecapParams.EnableExpandedExtendedRepliesFilterParam)
private[common] val excludeRecommendedRepliesToNonFollowedUsersGate: Gate[RecapQuery] =
RecapQuery.paramGate(
UtegLikedByTweetsParams.UTEGRecommendationsFilter.ExcludeRecommendedRepliesToNonFollowedUsersParam)
}
/**
* Filter hydrated tweets dynamically based on TweetKindOptions in the query.
*/
class TweetKindOptionHydratedTweetsFilterTransform(
useFollowGraphData: Boolean,
useSourceTweets: Boolean,
statsReceiver: StatsReceiver)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
import TweetKindOptionHydratedTweetsFilterTransform._
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val filters = convertToFilters(envelope)
val filterTransform = if (filters == TweetFilters.ValueSet.empty) {
FutureArrow.identity[CandidateEnvelope]
} else {
new HydratedTweetsFilterTransform(
outerFilters = filters,
innerFilters = TweetFilters.None,
useFollowGraphData = useFollowGraphData,
useSourceTweets = useSourceTweets,
statsReceiver = statsReceiver,
numRetweetsAllowed = HydratedTweetsFilterTransform.NumDuplicateRetweetsAllowed
)
}
filterTransform.apply(envelope)
}
/**
* Converts the given query options to equivalent TweetFilter values.
*
* Note:
* -- The semantic of TweetKindOption is opposite of that of TweetFilters.
* TweetKindOption values are of the form IncludeX. That is, they result in X being added.
* TweetFilters values specify what to exclude.
* -- IncludeExtendedReplies requires IncludeReplies to be also specified to be effective.
*/
private[common] def convertToFilters(envelope: CandidateEnvelope): TweetFilters.ValueSet = {
val queryOptions = envelope.query.options
val filters = mutable.Set.empty[TweetFilters.Value]
if (queryOptions.contains(TweetKindOption.IncludeReplies)) {
if (excludeRecommendedRepliesToNonFollowedUsersGate(
envelope.query) && envelope.query.utegLikedByTweetsOptions.isDefined) {
filters += TweetFilters.RecommendedRepliesToNotFollowedUsers
} else if (queryOptions.contains(TweetKindOption.IncludeExtendedReplies)) {
if (enableExpandedExtendedRepliesGate(envelope.query)) {
filters += TweetFilters.NotValidExpandedExtendedReplies
} else {
filters += TweetFilters.NotQualifiedExtendedReplies
}
} else {
filters += TweetFilters.ExtendedReplies
}
} else {
filters += TweetFilters.Replies
}
if (!queryOptions.contains(TweetKindOption.IncludeRetweets)) {
filters += TweetFilters.Retweets
}
TweetFilters.ValueSet.empty ++ filters
}
}

View File

@ -0,0 +1,30 @@
package com.twitter.timelineranker.common
import com.twitter.search.common.constants.thriftscala.ThriftLanguage
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelines.clients.manhattan.LanguageUtils
import com.twitter.timelines.clients.manhattan.UserMetadataClient
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.util.Future
import com.twitter.service.metastore.gen.thriftscala.UserLanguages
object UserLanguagesTransform {
val EmptyUserLanguagesFuture: Future[UserLanguages] =
Future.value(UserMetadataClient.EmptyUserLanguages)
}
/**
* FutureArrow which fetches user languages
* It should be run in parallel with the main pipeline which fetches and hydrates CandidateTweets
*/
class UserLanguagesTransform(handler: FailOpenHandler, userMetadataClient: UserMetadataClient)
extends FutureArrow[RecapQuery, Seq[ThriftLanguage]] {
override def apply(request: RecapQuery): Future[Seq[ThriftLanguage]] = {
import UserLanguagesTransform._
handler {
userMetadataClient.getUserLanguages(request.userId)
} { _: Throwable => EmptyUserLanguagesFuture }
}.map(LanguageUtils.computeLanguages(_))
}

View File

@ -0,0 +1,29 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelines.clients.gizmoduck.GizmoduckClient
import com.twitter.timelines.clients.gizmoduck.UserProfileInfo
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.util.Future
object UserProfileInfoTransform {
val EmptyUserProfileInfo: UserProfileInfo = UserProfileInfo(None, None, None, None)
val EmptyUserProfileInfoFuture: Future[UserProfileInfo] = Future.value(EmptyUserProfileInfo)
}
/**
* FutureArrow which fetches user profile info
* It should be run in parallel with the main pipeline which fetches and hydrates CandidateTweets
*/
class UserProfileInfoTransform(handler: FailOpenHandler, gizmoduckClient: GizmoduckClient)
extends FutureArrow[RecapQuery, UserProfileInfo] {
import UserProfileInfoTransform._
override def apply(request: RecapQuery): Future[UserProfileInfo] = {
handler {
gizmoduckClient.getProfileInfo(request.userId).map { profileInfoOpt =>
profileInfoOpt.getOrElse(EmptyUserProfileInfo)
}
} { _: Throwable => EmptyUserProfileInfoFuture }
}
}

View File

@ -0,0 +1,22 @@
package com.twitter.timelineranker.common
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.core.HydratedTweets
import com.twitter.timelines.visibility.VisibilityEnforcer
import com.twitter.util.Future
/**
* Transform which uses an instance of a VisiblityEnforcer to filter down HydratedTweets
*/
class VisibilityEnforcingTransform(visibilityEnforcer: VisibilityEnforcer)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
visibilityEnforcer.apply(Some(envelope.query.userId), envelope.hydratedTweets.outerTweets).map {
visibleTweets =>
val innerTweets = envelope.hydratedTweets.innerTweets
envelope.copy(
hydratedTweets = HydratedTweets(outerTweets = visibleTweets, innerTweets = innerTweets))
}
}
}

View File

@ -0,0 +1,65 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/twitter/storehaus:core",
"3rdparty/jvm/org/apache/thrift:libthrift",
"abdecider",
"cortex-tweet-annotate/service/src/main/thrift:thrift-scala",
"decider",
"featureswitches/featureswitches-core/src/main/scala",
"finagle-internal/mtls/src/main/scala/com/twitter/finagle/mtls/authentication",
"finagle/finagle-core/src/main",
"finagle/finagle-memcached/src/main/scala",
"finagle/finagle-stats",
"finagle/finagle-thrift/src/main/java",
"finagle/finagle-thrift/src/main/scala",
"finagle/finagle-thriftmux/src/main/scala",
"loglens/loglens-logging/src/main/scala",
"merlin/util/src/main/scala",
"servo/decider",
"servo/request/src/main/scala",
"src/thrift/com/twitter/gizmoduck:thrift-scala",
"src/thrift/com/twitter/manhattan:v1-scala",
"src/thrift/com/twitter/merlin:thrift-scala",
"src/thrift/com/twitter/recos/user_tweet_entity_graph:user_tweet_entity_graph-scala",
"src/thrift/com/twitter/search:earlybird-scala",
"src/thrift/com/twitter/socialgraph:thrift-scala",
"src/thrift/com/twitter/timelineranker:thrift-scala",
"src/thrift/com/twitter/timelineservice:thrift-scala",
"src/thrift/com/twitter/tweetypie:service-scala",
"src/thrift/com/twitter/tweetypie:tweet-scala",
"strato/src/main/scala/com/twitter/strato/client",
"timelineranker/server/config",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/clients",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/clients/content_features_cache",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelines:authorization",
"timelines:config",
"timelines:features",
"timelines:util",
"timelines:visibility",
"timelines/src/main/scala/com/twitter/timelines/clients/gizmoduck",
"timelines/src/main/scala/com/twitter/timelines/clients/manhattan",
"timelines/src/main/scala/com/twitter/timelines/clients/memcache_common",
"timelines/src/main/scala/com/twitter/timelines/clients/socialgraph",
"timelines/src/main/scala/com/twitter/timelines/clients/strato/realgraph",
"timelines/src/main/scala/com/twitter/timelines/clients/tweetypie",
"timelines/src/main/scala/com/twitter/timelines/config/configapi",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/util/logging",
"timelines/src/main/scala/com/twitter/timelines/util/stats",
"timelineservice/common/src/main/scala/com/twitter/timelineservice/model",
"twitter-server-internal",
"util/util-app",
"util/util-core:util-core-util",
"util/util-core/src/main/scala/com/twitter/conversions",
"util/util-stats/src/main/scala",
],
exports = [
"src/thrift/com/twitter/recos/user_tweet_entity_graph:user_tweet_entity_graph-scala",
],
)

View File

@ -0,0 +1,119 @@
package com.twitter.timelineranker.config
import com.twitter.conversions.DurationOps._
import com.twitter.util.Duration
import java.util.concurrent.TimeUnit
/**
* Information about a single method call.
*
* The purpose of this class is to allow one to express a call graph and latency associated with each (sub)call.
* Once a call graph is defined, calling getOverAllLatency() off the top level call returns total time taken by that call.
* That value can then be compared with the timeout budget allocated to that call to see if the
* value fits within the overall timeout budget of that call.
*
* This is useful in case of a complex call graph where it is hard to mentally estimate the effect on
* overall latency when updating timeout value of one or more sub-calls.
*
* @param methodName name of the called method.
* @param latency P999 Latency incurred in calling a service if the method calls an external service. Otherwise 0.
* @param dependsOn Other calls that this call depends on.
*/
case class Call(
methodName: String,
latency: Duration = 0.milliseconds,
dependsOn: Seq[Call] = Nil) {
/**
* Latency incurred in this call as well as recursively all calls this call depends on.
*/
def getOverAllLatency: Duration = {
val dependencyLatency = if (dependsOn.isEmpty) {
0.milliseconds
} else {
dependsOn.map(_.getOverAllLatency).max
}
latency + dependencyLatency
}
/**
* Call paths starting at this call and recursively traversing all dependencies.
* Typically used for debugging or logging.
*/
def getLatencyPaths: String = {
val sb = new StringBuilder
getLatencyPaths(sb, 1)
sb.toString
}
def getLatencyPaths(sb: StringBuilder, level: Int): Unit = {
sb.append(s"${getPrefix(level)} ${getLatencyString(getOverAllLatency)} $methodName\n")
if ((latency > 0.milliseconds) && !dependsOn.isEmpty) {
sb.append(s"${getPrefix(level + 1)} ${getLatencyString(latency)} self\n")
}
dependsOn.foreach(_.getLatencyPaths(sb, level + 1))
}
private def getLatencyString(latencyValue: Duration): String = {
val latencyMs = latencyValue.inUnit(TimeUnit.MILLISECONDS)
f"[$latencyMs%3d]"
}
private def getPrefix(level: Int): String = {
" " * (level * 4) + "--"
}
}
/**
* Information about the getRecapTweetCandidates call.
*
* Acronyms used:
* : call internal to TLR
* EB : Earlybird (search super root)
* GZ : Gizmoduck
* MH : Manhattan
* SGS : Social graph service
*
* The latency values are based on p9999 values observed over 1 week.
*/
object GetRecycledTweetCandidatesCall {
val getUserProfileInfo: Call = Call("GZ.getUserProfileInfo", 200.milliseconds)
val getUserLanguages: Call = Call("MH.getUserLanguages", 300.milliseconds) // p99: 15ms
val getFollowing: Call = Call("SGS.getFollowing", 250.milliseconds) // p99: 75ms
val getMutuallyFollowing: Call =
Call("SGS.getMutuallyFollowing", 400.milliseconds, Seq(getFollowing)) // p99: 100
val getVisibilityProfiles: Call =
Call("SGS.getVisibilityProfiles", 400.milliseconds, Seq(getFollowing)) // p99: 100
val getVisibilityData: Call = Call(
"getVisibilityData",
dependsOn = Seq(getFollowing, getMutuallyFollowing, getVisibilityProfiles)
)
val getTweetsForRecapRegular: Call =
Call("EB.getTweetsForRecap(regular)", 500.milliseconds, Seq(getVisibilityData)) // p99: 250
val getTweetsForRecapProtected: Call =
Call("EB.getTweetsForRecap(protected)", 250.milliseconds, Seq(getVisibilityData)) // p99: 150
val getSearchResults: Call =
Call("getSearchResults", dependsOn = Seq(getTweetsForRecapRegular, getTweetsForRecapProtected))
val getTweetsScoredForRecap: Call =
Call("EB.getTweetsScoredForRecap", 400.milliseconds, Seq(getSearchResults)) // p99: 100
val hydrateSearchResults: Call = Call("hydrateSearchResults")
val getSourceTweetSearchResults: Call =
Call("getSourceTweetSearchResults", dependsOn = Seq(getSearchResults))
val hydrateTweets: Call =
Call("hydrateTweets", dependsOn = Seq(getSearchResults, hydrateSearchResults))
val hydrateSourceTweets: Call =
Call("hydrateSourceTweets", dependsOn = Seq(getSourceTweetSearchResults, hydrateSearchResults))
val topLevel: Call = Call(
"getRecapTweetCandidates",
dependsOn = Seq(
getUserProfileInfo,
getUserLanguages,
getVisibilityData,
getSearchResults,
hydrateSearchResults,
hydrateSourceTweets
)
)
}

View File

@ -0,0 +1,287 @@
package com.twitter.timelineranker.config
import com.twitter.timelineranker.decider.DeciderKey._
import com.twitter.timelines.authorization.TrustedPermission
import com.twitter.timelines.authorization.RateLimitingTrustedPermission
import com.twitter.timelines.authorization.RateLimitingUntrustedPermission
import com.twitter.timelines.authorization.ClientDetails
object ClientAccessPermissions {
// We want timelineranker locked down for requests outside of what's defined here.
val DefaultRateLimit = 0d
def unknown(name: String): ClientDetails = {
ClientDetails(name, RateLimitingUntrustedPermission(RateLimitOverrideUnknown, DefaultRateLimit))
}
val All: Seq[ClientDetails] = Seq(
/**
* Production clients for timelinemixer.
*/
new ClientDetails(
"timelinemixer.recap.prod",
RateLimitingTrustedPermission(AllowTimelineMixerRecapProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.recycled.prod",
RateLimitingTrustedPermission(AllowTimelineMixerRecycledProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.hydrate.prod",
RateLimitingTrustedPermission(AllowTimelineMixerHydrateProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.hydrate_recos.prod",
RateLimitingTrustedPermission(AllowTimelineMixerHydrateRecosProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.seed_author_ids.prod",
RateLimitingTrustedPermission(AllowTimelineMixerSeedAuthorsProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.simcluster.prod",
RateLimitingTrustedPermission(AllowTimelineMixerSimclusterProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.entity_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineMixerEntityTweetsProd),
protectedWriteAccess = TrustedPermission
),
/**
* This client is whitelisted for timelinemixer only as it used by
* List injection service which will not be migrated to timelinescorer.
*/
new ClientDetails(
"timelinemixer.list.prod",
RateLimitingTrustedPermission(AllowTimelineMixerListProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.list_tweet.prod",
RateLimitingTrustedPermission(AllowTimelineMixerListTweetProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.community.prod",
RateLimitingTrustedPermission(AllowTimelineMixerCommunityProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.community_tweet.prod",
RateLimitingTrustedPermission(AllowTimelineMixerCommunityTweetProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.uteg_liked_by_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineMixerUtegLikedByTweetsProd),
protectedWriteAccess = TrustedPermission
),
/**
* Production clients for timelinescorer. Most of these clients have their
* equivalents under the timelinemixer scope (with exception of list injection
* client).
*/
new ClientDetails(
"timelinescorer.recap.prod",
RateLimitingTrustedPermission(AllowTimelineMixerRecapProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.recycled.prod",
RateLimitingTrustedPermission(AllowTimelineMixerRecycledProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.hydrate.prod",
RateLimitingTrustedPermission(AllowTimelineMixerHydrateProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.hydrate_recos.prod",
RateLimitingTrustedPermission(AllowTimelineMixerHydrateRecosProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.seed_author_ids.prod",
RateLimitingTrustedPermission(AllowTimelineMixerSeedAuthorsProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.simcluster.prod",
RateLimitingTrustedPermission(AllowTimelineMixerSimclusterProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.entity_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineMixerEntityTweetsProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.list_tweet.prod",
RateLimitingTrustedPermission(AllowTimelineMixerListTweetProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.uteg_liked_by_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineMixerUtegLikedByTweetsProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelineservice.prod",
RateLimitingTrustedPermission(AllowTimelineServiceProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.hydrate_tweet_scoring.prod",
RateLimitingTrustedPermission(AllowTimelineScorerHydrateTweetScoringProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.community_tweet.prod",
RateLimitingTrustedPermission(AllowTimelineMixerCommunityTweetProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.recommended_trend_tweet.prod",
RateLimitingTrustedPermission(AllowTimelineScorerRecommendedTrendTweetProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.rec_topic_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineScorerRecTopicTweetsProd),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.popular_topic_tweets.prod",
RateLimitingTrustedPermission(AllowTimelineScorerPopularTopicTweetsProd),
protectedWriteAccess = TrustedPermission
),
/**
* TimelineRanker utilities. Traffic proxy, warmups, and console.
*/
new ClientDetails(
"timelineranker.proxy",
RateLimitingTrustedPermission(AllowTimelineRankerProxy),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
TimelineRankerConstants.WarmupClientName,
RateLimitingTrustedPermission(AllowTimelineRankerWarmup),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
TimelineRankerConstants.ForwardedClientName,
RateLimitingTrustedPermission(AllowTimelineRankerWarmup),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelineranker.console",
RateLimitingUntrustedPermission(RateLimitOverrideUnknown, 1d),
protectedWriteAccess = TrustedPermission
),
/**
* Staging clients.
*/
new ClientDetails(
"timelinemixer.recap.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.recycled.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.hydrate.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.hydrate_recos.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.seed_author_ids.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.simcluster.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.entity_tweets.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.list.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.list_tweet.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.community.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.community_tweet.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.community_tweet.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.recommended_trend_tweet.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.uteg_liked_by_tweets.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinemixer.entity_tweets.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.hydrate_tweet_scoring.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.rec_topic_tweets.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelinescorer.popular_topic_tweets.staging",
RateLimitingTrustedPermission(AllowTimelineMixerStaging),
protectedWriteAccess = TrustedPermission
),
new ClientDetails(
"timelineservice.staging",
RateLimitingTrustedPermission(AllowTimelineServiceStaging),
protectedWriteAccess = TrustedPermission
)
)
}

View File

@ -0,0 +1,86 @@
package com.twitter.timelineranker.config
import com.twitter.servo.util.Gate
import com.twitter.timelineranker.clients.ScopedCortexTweetQueryServiceClientFactory
import com.twitter.timelines.clients.gizmoduck.ScopedGizmoduckClientFactory
import com.twitter.timelines.clients.manhattan.ScopedUserMetadataClientFactory
import com.twitter.timelines.clients.socialgraph.ScopedSocialGraphClientFactory
import com.twitter.timelines.clients.strato.realgraph.ScopedRealGraphClientFactory
import com.twitter.timelines.clients.tweetypie.AdditionalFieldConfig
import com.twitter.timelines.clients.tweetypie.ScopedTweetyPieClientFactory
import com.twitter.timelines.visibility.VisibilityEnforcerFactory
import com.twitter.timelines.visibility.VisibilityProfileHydratorFactory
import com.twitter.tweetypie.thriftscala.{Tweet => TTweet}
class ClientWrapperFactories(config: RuntimeConfiguration) {
private[this] val statsReceiver = config.statsReceiver
val cortexTweetQueryServiceClientFactory: ScopedCortexTweetQueryServiceClientFactory =
new ScopedCortexTweetQueryServiceClientFactory(
config.underlyingClients.cortexTweetQueryServiceClient,
statsReceiver = statsReceiver
)
val gizmoduckClientFactory: ScopedGizmoduckClientFactory = new ScopedGizmoduckClientFactory(
config.underlyingClients.gizmoduckClient,
statsReceiver = statsReceiver
)
val socialGraphClientFactory: ScopedSocialGraphClientFactory = new ScopedSocialGraphClientFactory(
config.underlyingClients.sgsClient,
statsReceiver
)
val visibilityEnforcerFactory: VisibilityEnforcerFactory = new VisibilityEnforcerFactory(
gizmoduckClientFactory,
socialGraphClientFactory,
statsReceiver
)
val tweetyPieAdditionalFieldsToDisable: Seq[Short] = Seq(
TTweet.MediaTagsField.id,
TTweet.SchedulingInfoField.id,
TTweet.EscherbirdEntityAnnotationsField.id,
TTweet.CardReferenceField.id,
TTweet.SelfPermalinkField.id,
TTweet.ExtendedTweetMetadataField.id,
TTweet.CommunitiesField.id,
TTweet.VisibleTextRangeField.id
)
val tweetyPieHighQoSClientFactory: ScopedTweetyPieClientFactory =
new ScopedTweetyPieClientFactory(
tweetyPieClient = config.underlyingClients.tweetyPieHighQoSClient,
additionalFieldConfig = AdditionalFieldConfig(
fieldDisablingGates = tweetyPieAdditionalFieldsToDisable.map(_ -> Gate.False).toMap
),
includePartialResults = Gate.False,
statsReceiver = statsReceiver
)
val tweetyPieLowQoSClientFactory: ScopedTweetyPieClientFactory = new ScopedTweetyPieClientFactory(
tweetyPieClient = config.underlyingClients.tweetyPieLowQoSClient,
additionalFieldConfig = AdditionalFieldConfig(
fieldDisablingGates = tweetyPieAdditionalFieldsToDisable.map(_ -> Gate.False).toMap
),
includePartialResults = Gate.False,
statsReceiver = statsReceiver
)
val userMetadataClientFactory: ScopedUserMetadataClientFactory =
new ScopedUserMetadataClientFactory(
config.underlyingClients.manhattanStarbuckClient,
TimelineRankerConstants.ManhattanStarbuckAppId,
statsReceiver
)
val visibilityProfileHydratorFactory: VisibilityProfileHydratorFactory =
new VisibilityProfileHydratorFactory(
gizmoduckClientFactory,
socialGraphClientFactory,
statsReceiver
)
val realGraphClientFactory =
new ScopedRealGraphClientFactory(config.underlyingClients.stratoClient, statsReceiver)
}

View File

@ -0,0 +1,11 @@
package com.twitter.timelineranker.config
import com.twitter.storehaus.Store
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelines.model.TweetId
class ClientWrappers(config: RuntimeConfiguration) {
private[this] val backendClientConfiguration = config.underlyingClients
val contentFeaturesCache: Store[TweetId, ContentFeatures] =
backendClientConfiguration.contentFeaturesCache
}

View File

@ -0,0 +1,158 @@
package com.twitter.timelineranker.config
import com.twitter.conversions.DurationOps._
import com.twitter.conversions.PercentOps._
import com.twitter.cortex_tweet_annotate.thriftscala.CortexTweetQueryService
import com.twitter.finagle.ssl.OpportunisticTls
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finagle.thrift.ClientId
import com.twitter.finagle.util.DefaultTimer
import com.twitter.gizmoduck.thriftscala.{UserService => Gizmoduck}
import com.twitter.manhattan.v1.thriftscala.{ManhattanCoordinator => ManhattanV1}
import com.twitter.merlin.thriftscala.UserRolesService
import com.twitter.recos.user_tweet_entity_graph.thriftscala.UserTweetEntityGraph
import com.twitter.socialgraph.thriftscala.SocialGraphService
import com.twitter.storehaus.Store
import com.twitter.strato.client.Strato
import com.twitter.strato.client.{Client => StratoClient}
import com.twitter.timelineranker.clients.content_features_cache.ContentFeaturesMemcacheBuilder
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelineranker.thriftscala.TimelineRanker
import com.twitter.timelines.clients.memcache_common.StorehausMemcacheConfig
import com.twitter.timelines.model.TweetId
import com.twitter.timelineservice.thriftscala.TimelineService
import com.twitter.tweetypie.thriftscala.{TweetService => TweetyPie}
import com.twitter.util.Timer
import org.apache.thrift.protocol.TCompactProtocol
class DefaultUnderlyingClientConfiguration(flags: TimelineRankerFlags, statsReceiver: StatsReceiver)
extends UnderlyingClientConfiguration(flags, statsReceiver) { top =>
val timer: Timer = DefaultTimer
val twCachePrefix = "/srv#/prod/local/cache"
override val cortexTweetQueryServiceClient: CortexTweetQueryService.MethodPerEndpoint = {
methodPerEndpointClient[
CortexTweetQueryService.ServicePerEndpoint,
CortexTweetQueryService.MethodPerEndpoint](
thriftMuxClientBuilder("cortex-tweet-query", requireMutualTls = true)
.dest("/s/cortex-tweet-annotate/cortex-tweet-query")
.requestTimeout(200.milliseconds)
.timeout(500.milliseconds)
)
}
override val gizmoduckClient: Gizmoduck.MethodPerEndpoint = {
methodPerEndpointClient[Gizmoduck.ServicePerEndpoint, Gizmoduck.MethodPerEndpoint](
thriftMuxClientBuilder("gizmoduck", requireMutualTls = true)
.dest("/s/gizmoduck/gizmoduck")
.requestTimeout(400.milliseconds)
.timeout(900.milliseconds)
)
}
override lazy val manhattanStarbuckClient: ManhattanV1.MethodPerEndpoint = {
methodPerEndpointClient[ManhattanV1.ServicePerEndpoint, ManhattanV1.MethodPerEndpoint](
thriftMuxClientBuilder("manhattan.starbuck", requireMutualTls = true)
.dest("/s/manhattan/starbuck.native-thrift")
.requestTimeout(600.millis)
)
}
override val sgsClient: SocialGraphService.MethodPerEndpoint = {
methodPerEndpointClient[
SocialGraphService.ServicePerEndpoint,
SocialGraphService.MethodPerEndpoint](
thriftMuxClientBuilder("socialgraph", requireMutualTls = true)
.dest("/s/socialgraph/socialgraph")
.requestTimeout(350.milliseconds)
.timeout(700.milliseconds)
)
}
override lazy val timelineRankerForwardingClient: TimelineRanker.FinagledClient =
new TimelineRanker.FinagledClient(
thriftMuxClientBuilder(
TimelineRankerConstants.ForwardedClientName,
ClientId(TimelineRankerConstants.ForwardedClientName),
protocolFactoryOption = Some(new TCompactProtocol.Factory()),
requireMutualTls = true
).dest("/s/timelineranker/timelineranker:compactthrift").build(),
protocolFactory = new TCompactProtocol.Factory()
)
override val timelineServiceClient: TimelineService.MethodPerEndpoint = {
methodPerEndpointClient[TimelineService.ServicePerEndpoint, TimelineService.MethodPerEndpoint](
thriftMuxClientBuilder("timelineservice", requireMutualTls = true)
.dest("/s/timelineservice/timelineservice")
.requestTimeout(600.milliseconds)
.timeout(800.milliseconds)
)
}
override val tweetyPieHighQoSClient: TweetyPie.MethodPerEndpoint = {
methodPerEndpointClient[TweetyPie.ServicePerEndpoint, TweetyPie.MethodPerEndpoint](
thriftMuxClientBuilder("tweetypieHighQoS", requireMutualTls = true)
.dest("/s/tweetypie/tweetypie")
.requestTimeout(450.milliseconds)
.timeout(800.milliseconds),
maxExtraLoadPercent = Some(1.percent)
)
}
/**
* Provide less costly TweetPie client with the trade-off of reduced quality. Intended for use cases
* which are not essential for successful completion of timeline requests. Currently this client differs
* from the highQoS endpoint by having retries count set to 1 instead of 2.
*/
override val tweetyPieLowQoSClient: TweetyPie.MethodPerEndpoint = {
methodPerEndpointClient[TweetyPie.ServicePerEndpoint, TweetyPie.MethodPerEndpoint](
thriftMuxClientBuilder("tweetypieLowQoS", requireMutualTls = true)
.dest("/s/tweetypie/tweetypie")
.retryPolicy(mkRetryPolicy(1)) // override default value
.requestTimeout(450.milliseconds)
.timeout(800.milliseconds),
maxExtraLoadPercent = Some(1.percent)
)
}
override val userRolesServiceClient: UserRolesService.MethodPerEndpoint = {
methodPerEndpointClient[
UserRolesService.ServicePerEndpoint,
UserRolesService.MethodPerEndpoint](
thriftMuxClientBuilder("merlin", requireMutualTls = true)
.dest("/s/merlin/merlin")
.requestTimeout(1.second)
)
}
lazy val contentFeaturesCache: Store[TweetId, ContentFeatures] =
new ContentFeaturesMemcacheBuilder(
config = new StorehausMemcacheConfig(
destName = s"$twCachePrefix/timelines_content_features:twemcaches",
keyPrefix = "",
requestTimeout = 50.milliseconds,
numTries = 1,
globalTimeout = 60.milliseconds,
tcpConnectTimeout = 50.milliseconds,
connectionAcquisitionTimeout = 25.milliseconds,
numPendingRequests = 100,
isReadOnly = false,
serviceIdentifier = serviceIdentifier
),
ttl = 48.hours,
statsReceiver
).build
override val userTweetEntityGraphClient: UserTweetEntityGraph.FinagledClient =
new UserTweetEntityGraph.FinagledClient(
thriftMuxClientBuilder("user_tweet_entity_graph", requireMutualTls = true)
.dest("/s/cassowary/user_tweet_entity_graph")
.retryPolicy(mkRetryPolicy(2))
.requestTimeout(300.milliseconds)
.build()
)
override val stratoClient: StratoClient =
Strato.client.withMutualTls(serviceIdentifier, OpportunisticTls.Required).build()
}

View File

@ -0,0 +1,13 @@
package com.twitter.timelineranker.config
import com.twitter.timelines.util.stats.RequestScope
object RequestScopes {
val HomeTimelineMaterialization: RequestScope = RequestScope("homeMaterialization")
val InNetworkTweetSource: RequestScope = RequestScope("inNetworkTweet")
val RecapHydrationSource: RequestScope = RequestScope("recapHydration")
val RecapAuthorSource: RequestScope = RequestScope("recapAuthor")
val ReverseChronHomeTimelineSource: RequestScope = RequestScope("reverseChronHomeTimelineSource")
val EntityTweetsSource: RequestScope = RequestScope("entityTweets")
val UtegLikedByTweetsSource: RequestScope = RequestScope("utegLikedByTweets")
}

View File

@ -0,0 +1,133 @@
package com.twitter.timelineranker.config
import com.twitter.abdecider.ABDeciderFactory
import com.twitter.abdecider.LoggingABDecider
import com.twitter.decider.Decider
import com.twitter.featureswitches.Value
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.util.Effect
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelines.authorization.TimelinesClientRequestAuthorizer
import com.twitter.timelines.config._
import com.twitter.timelines.config.configapi._
import com.twitter.timelines.features._
import com.twitter.timelines.util.ImpressionCountingABDecider
import com.twitter.timelines.util.logging.Scribe
import com.twitter.util.Await
import com.twitter.servo.util.Gate
import com.twitter.timelines.model.UserId
trait ClientProvider {
def clientWrappers: ClientWrappers
def underlyingClients: UnderlyingClientConfiguration
}
trait UtilityProvider {
def abdecider: LoggingABDecider
def clientRequestAuthorizer: TimelinesClientRequestAuthorizer
def configStore: ConfigStore
def decider: Decider
def deciderGateBuilder: DeciderGateBuilder
def employeeGate: UserRolesGate.EmployeeGate
def configApiConfiguration: ConfigApiConfiguration
def statsReceiver: StatsReceiver
def whitelist: UserList
}
trait RuntimeConfiguration extends ClientProvider with UtilityProvider with ConfigUtils {
def isProd: Boolean
def maxConcurrency: Int
def clientEventScribe: Effect[String]
def clientWrapperFactories: ClientWrapperFactories
}
class RuntimeConfigurationImpl(
flags: TimelineRankerFlags,
configStoreFactory: DynamicConfigStoreFactory,
val decider: Decider,
val forcedFeatureValues: Map[String, Value] = Map.empty[String, Value],
val statsReceiver: StatsReceiver)
extends RuntimeConfiguration {
// Creates and initialize config store as early as possible so other parts could have a dependency on it for settings.
override val configStore: DynamicConfigStore =
configStoreFactory.createDcEnvAwareFileBasedConfigStore(
relativeConfigFilePath = "timelines/timelineranker/service_settings.yml",
dc = flags.getDatacenter,
env = flags.getEnv,
configBusConfig = ConfigBusProdConfig,
onUpdate = ConfigStore.NullOnUpdateCallback,
statsReceiver = statsReceiver
)
Await.result(configStore.init)
val environment: Env.Value = flags.getEnv
override val isProd: Boolean = isProdEnv(environment)
val datacenter: Datacenter.Value = flags.getDatacenter
val abDeciderPath = "/usr/local/config/abdecider/abdecider.yml"
override val maxConcurrency: Int = flags.maxConcurrency()
val deciderGateBuilder: DeciderGateBuilder = new DeciderGateBuilder(decider)
val clientRequestAuthorizer: TimelinesClientRequestAuthorizer =
new TimelinesClientRequestAuthorizer(
deciderGateBuilder = deciderGateBuilder,
clientDetails = ClientAccessPermissions.All,
unknownClientDetails = ClientAccessPermissions.unknown,
clientAuthorizationGate =
deciderGateBuilder.linearGate(DeciderKey.ClientRequestAuthorization),
clientWriteWhitelistGate = deciderGateBuilder.linearGate(DeciderKey.ClientWriteWhitelist),
globalCapacityQPS = flags.requestRateLimit(),
statsReceiver = statsReceiver
)
override val clientEventScribe = Scribe.clientEvent(isProd, statsReceiver)
val abdecider: LoggingABDecider = new ImpressionCountingABDecider(
abdecider = ABDeciderFactory.withScribeEffect(
abDeciderYmlPath = abDeciderPath,
scribeEffect = clientEventScribe,
decider = None,
environment = Some("production"),
).buildWithLogging(),
statsReceiver = statsReceiver
)
val underlyingClients: UnderlyingClientConfiguration = buildUnderlyingClientConfiguration
val clientWrappers: ClientWrappers = new ClientWrappers(this)
override val clientWrapperFactories: ClientWrapperFactories = new ClientWrapperFactories(this)
private[this] val userRolesCacheFactory = new UserRolesCacheFactory(
userRolesService = underlyingClients.userRolesServiceClient,
statsReceiver = statsReceiver
)
override val whitelist: Whitelist = Whitelist(
configStoreFactory = configStoreFactory,
userRolesCacheFactory = userRolesCacheFactory,
statsReceiver = statsReceiver
)
override val employeeGate: Gate[UserId] = UserRolesGate(
userRolesCacheFactory.create(UserRoles.EmployeesRoleName)
)
private[this] val featureRecipientFactory =
new UserRolesCachingFeatureRecipientFactory(userRolesCacheFactory, statsReceiver)
override val configApiConfiguration: FeatureSwitchesV2ConfigApiConfiguration =
FeatureSwitchesV2ConfigApiConfiguration(
datacenter = flags.getDatacenter,
serviceName = ServiceName.TimelineRanker,
abdecider = abdecider,
featureRecipientFactory = featureRecipientFactory,
forcedValues = forcedFeatureValues,
statsReceiver = statsReceiver
)
private[this] def buildUnderlyingClientConfiguration: UnderlyingClientConfiguration = {
environment match {
case Env.prod => new DefaultUnderlyingClientConfiguration(flags, statsReceiver)
case _ => new StagingUnderlyingClientConfiguration(flags, statsReceiver)
}
}
}

View File

@ -0,0 +1,6 @@
package com.twitter.timelineranker.config
import com.twitter.finagle.stats.StatsReceiver
class StagingUnderlyingClientConfiguration(flags: TimelineRankerFlags, statsReceiver: StatsReceiver)
extends DefaultUnderlyingClientConfiguration(flags, statsReceiver)

View File

@ -0,0 +1,8 @@
package com.twitter.timelineranker.config
object TimelineRankerConstants {
val ClientPrefix = "timelineranker."
val ManhattanStarbuckAppId = "timelineranker"
val WarmupClientName = "timelineranker.warmup"
val ForwardedClientName = "timelineranker.forwarded"
}

View File

@ -0,0 +1,72 @@
package com.twitter.timelineranker.config
import com.twitter.app.Flags
import com.twitter.finagle.mtls.authentication.EmptyServiceIdentifier
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
import com.twitter.timelines.config.CommonFlags
import com.twitter.timelines.config.ConfigUtils
import com.twitter.timelines.config.Datacenter
import com.twitter.timelines.config.Env
import com.twitter.timelines.config.ProvidesServiceIdentifier
import java.net.InetSocketAddress
import com.twitter.app.Flag
class TimelineRankerFlags(flag: Flags)
extends CommonFlags(flag)
with ConfigUtils
with ProvidesServiceIdentifier {
val dc: Flag[String] = flag(
"dc",
"smf1",
"Name of data center in which this instance will execute"
)
val environment: Flag[String] = flag(
"environment",
"devel",
"The mesos environment in which this instance will be running"
)
val maxConcurrency: Flag[Int] = flag(
"maxConcurrency",
200,
"Maximum concurrent requests"
)
val servicePort: Flag[InetSocketAddress] = flag(
"service.port",
new InetSocketAddress(8287),
"Port number that this thrift service will listen on"
)
val serviceCompactPort: Flag[InetSocketAddress] = flag(
"service.compact.port",
new InetSocketAddress(8288),
"Port number that the TCompactProtocol-based thrift service will listen on"
)
val serviceIdentifier: Flag[ServiceIdentifier] = flag[ServiceIdentifier](
"service.identifier",
EmptyServiceIdentifier,
"service identifier for this service for use with mutual TLS, " +
"format is expected to be -service.identifier=\"role:service:environment:zone\""
)
val opportunisticTlsLevel = flag[String](
"opportunistic.tls.level",
"desired",
"The server's OpportunisticTls level."
)
val requestRateLimit: Flag[Double] = flag[Double](
"requestRateLimit",
1000.0,
"Request rate limit to be used by the client request authorizer"
)
val enableThriftmuxCompression = flag(
"enableThriftmuxServerCompression",
true,
"build server with thriftmux compression enabled"
)
def getDatacenter: Datacenter.Value = getDC(dc())
def getEnv: Env.Value = getEnv(environment())
override def getServiceIdentifier: ServiceIdentifier = serviceIdentifier()
}

View File

@ -0,0 +1,107 @@
package com.twitter.timelineranker.config
import com.twitter.cortex_tweet_annotate.thriftscala.CortexTweetQueryService
import com.twitter.finagle.Service
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
import com.twitter.finagle.service.RetryPolicy
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finagle.thrift.ClientId
import com.twitter.finagle.thrift.ThriftClientRequest
import com.twitter.gizmoduck.thriftscala.{UserService => Gizmoduck}
import com.twitter.manhattan.v1.thriftscala.{ManhattanCoordinator => ManhattanV1}
import com.twitter.merlin.thriftscala.UserRolesService
import com.twitter.recos.user_tweet_entity_graph.thriftscala.UserTweetEntityGraph
import com.twitter.search.earlybird.thriftscala.EarlybirdService
import com.twitter.socialgraph.thriftscala.SocialGraphService
import com.twitter.storehaus.Store
import com.twitter.strato.client.{Client => StratoClient}
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelineranker.thriftscala.TimelineRanker
import com.twitter.timelines.config.ConfigUtils
import com.twitter.timelines.config.TimelinesUnderlyingClientConfiguration
import com.twitter.timelines.model.TweetId
import com.twitter.timelineservice.thriftscala.TimelineService
import com.twitter.tweetypie.thriftscala.{TweetService => TweetyPie}
import com.twitter.util.Duration
import com.twitter.util.Try
import org.apache.thrift.protocol.TCompactProtocol
abstract class UnderlyingClientConfiguration(
flags: TimelineRankerFlags,
val statsReceiver: StatsReceiver)
extends TimelinesUnderlyingClientConfiguration
with ConfigUtils {
lazy val thriftClientId: ClientId = timelineRankerClientId()
override lazy val serviceIdentifier: ServiceIdentifier = flags.getServiceIdentifier
def timelineRankerClientId(scope: Option[String] = None): ClientId = {
clientIdWithScopeOpt("timelineranker", flags.getEnv, scope)
}
def createEarlybirdClient(
scope: String,
requestTimeout: Duration,
timeout: Duration,
retryPolicy: RetryPolicy[Try[Nothing]]
): EarlybirdService.MethodPerEndpoint = {
val scopedName = s"earlybird/$scope"
methodPerEndpointClient[
EarlybirdService.ServicePerEndpoint,
EarlybirdService.MethodPerEndpoint](
thriftMuxClientBuilder(
scopedName,
protocolFactoryOption = Some(new TCompactProtocol.Factory),
requireMutualTls = true)
.dest("/s/earlybird-root-superroot/root-superroot")
.timeout(timeout)
.requestTimeout(requestTimeout)
.retryPolicy(retryPolicy)
)
}
def createEarlybirdRealtimeCgClient(
scope: String,
requestTimeout: Duration,
timeout: Duration,
retryPolicy: RetryPolicy[Try[Nothing]]
): EarlybirdService.MethodPerEndpoint = {
val scopedName = s"earlybird/$scope"
methodPerEndpointClient[
EarlybirdService.ServicePerEndpoint,
EarlybirdService.MethodPerEndpoint](
thriftMuxClientBuilder(
scopedName,
protocolFactoryOption = Some(new TCompactProtocol.Factory),
requireMutualTls = true)
.dest("/s/earlybird-rootrealtimecg/root-realtime_cg")
.timeout(timeout)
.requestTimeout(requestTimeout)
.retryPolicy(retryPolicy)
)
}
def cortexTweetQueryServiceClient: CortexTweetQueryService.MethodPerEndpoint
def gizmoduckClient: Gizmoduck.MethodPerEndpoint
def manhattanStarbuckClient: ManhattanV1.MethodPerEndpoint
def sgsClient: SocialGraphService.MethodPerEndpoint
def timelineRankerForwardingClient: TimelineRanker.FinagledClient
def timelineServiceClient: TimelineService.MethodPerEndpoint
def tweetyPieHighQoSClient: TweetyPie.MethodPerEndpoint
def tweetyPieLowQoSClient: TweetyPie.MethodPerEndpoint
def userRolesServiceClient: UserRolesService.MethodPerEndpoint
def contentFeaturesCache: Store[TweetId, ContentFeatures]
def userTweetEntityGraphClient: UserTweetEntityGraph.FinagledClient
def stratoClient: StratoClient
def darkTrafficProxy: Option[Service[ThriftClientRequest, Array[Byte]]] = {
if (flags.darkTrafficDestination.trim.nonEmpty) {
Some(darkTrafficClient(flags.darkTrafficDestination))
} else {
None
}
}
}

View File

@ -0,0 +1,13 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/core",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelines/src/main/scala/com/twitter/timelines/model/types",
],
)

View File

@ -0,0 +1,10 @@
package com.twitter.timelineranker
import com.twitter.timelineranker.core.FutureDependencyTransformer
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelines.model.TweetId
package object contentfeatures {
type ContentFeaturesProvider =
FutureDependencyTransformer[Seq[TweetId], Map[TweetId, ContentFeatures]]
}

View File

@ -0,0 +1,24 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"src/thrift/com/twitter/recos/user_tweet_entity_graph:user_tweet_entity_graph-scala",
"src/thrift/com/twitter/search:earlybird-scala",
"src/thrift/com/twitter/search/common:constants-scala",
"src/thrift/com/twitter/search/common:features-scala",
"timelineranker/common:model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelines/src/main/scala/com/twitter/timelines/clients/gizmoduck",
"timelines/src/main/scala/com/twitter/timelines/model/tweet",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/visibility/model",
"util/util-core:util-core-util",
],
exports = [
"src/thrift/com/twitter/recos/user_tweet_entity_graph:user_tweet_entity_graph-scala",
],
)

View File

@ -0,0 +1,24 @@
package com.twitter.timelineranker.core
import com.twitter.recos.user_tweet_entity_graph.thriftscala.TweetRecommendation
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelines.model.TweetId
object CandidateEnvelope {
val EmptySearchResults: Seq[ThriftSearchResult] = Seq.empty[ThriftSearchResult]
val EmptyHydratedTweets: HydratedTweets = HydratedTweets(Seq.empty, Seq.empty)
val EmptyUtegResults: Map[TweetId, TweetRecommendation] = Map.empty[TweetId, TweetRecommendation]
}
case class CandidateEnvelope(
query: RecapQuery,
searchResults: Seq[ThriftSearchResult] = CandidateEnvelope.EmptySearchResults,
utegResults: Map[TweetId, TweetRecommendation] = CandidateEnvelope.EmptyUtegResults,
hydratedTweets: HydratedTweets = CandidateEnvelope.EmptyHydratedTweets,
followGraphData: FollowGraphDataFuture = FollowGraphDataFuture.EmptyFollowGraphDataFuture,
// The source tweets are
// - the retweeted tweet, for retweets
// - the inReplyTo tweet, for extended replies
sourceSearchResults: Seq[ThriftSearchResult] = CandidateEnvelope.EmptySearchResults,
sourceHydratedTweets: HydratedTweets = CandidateEnvelope.EmptyHydratedTweets)

View File

@ -0,0 +1,34 @@
package com.twitter.timelineranker.core
import com.twitter.timelines.model.UserId
/**
* Follow graph details of a given user. Includes users followed, but also followed users in various
* states of mute.
*
* @param userId ID of a given user.
* @param followedUserIds IDs of users who the given user follows.
* @param mutuallyFollowingUserIds A subset of followedUserIds where followed users follow back the given user.
* @param mutedUserIds A subset of followedUserIds that the given user has muted.
* @param retweetsMutedUserIds A subset of followedUserIds whose retweets are muted by the given user.
*/
case class FollowGraphData(
userId: UserId,
followedUserIds: Seq[UserId],
mutuallyFollowingUserIds: Set[UserId],
mutedUserIds: Set[UserId],
retweetsMutedUserIds: Set[UserId]) {
val filteredFollowedUserIds: Seq[UserId] = followedUserIds.filterNot(mutedUserIds)
val allUserIds: Seq[UserId] = filteredFollowedUserIds :+ userId
val inNetworkUserIds: Seq[UserId] = followedUserIds :+ userId
}
object FollowGraphData {
val Empty: FollowGraphData = FollowGraphData(
0L,
Seq.empty[UserId],
Set.empty[UserId],
Set.empty[UserId],
Set.empty[UserId]
)
}

View File

@ -0,0 +1,53 @@
package com.twitter.timelineranker.core
import com.twitter.timelines.model.UserId
import com.twitter.util.Future
/**
* Similar to FollowGraphData but makes available its fields as separate futures.
*/
case class FollowGraphDataFuture(
userId: UserId,
followedUserIdsFuture: Future[Seq[UserId]],
mutuallyFollowingUserIdsFuture: Future[Set[UserId]],
mutedUserIdsFuture: Future[Set[UserId]],
retweetsMutedUserIdsFuture: Future[Set[UserId]]) {
def inNetworkUserIdsFuture: Future[Seq[UserId]] = followedUserIdsFuture.map(_ :+ userId)
def get(): Future[FollowGraphData] = {
Future
.join(
followedUserIdsFuture,
mutuallyFollowingUserIdsFuture,
mutedUserIdsFuture,
retweetsMutedUserIdsFuture
)
.map {
case (followedUserIds, mutuallyFollowingUserIds, mutedUserIds, retweetsMutedUserIds) =>
FollowGraphData(
userId,
followedUserIds,
mutuallyFollowingUserIds,
mutedUserIds,
retweetsMutedUserIds
)
}
}
}
object FollowGraphDataFuture {
private def mkEmptyFuture(field: String) = {
Future.exception(
new IllegalStateException(s"FollowGraphDataFuture field $field accessed without being set")
)
}
val EmptyFollowGraphDataFuture: FollowGraphDataFuture = FollowGraphDataFuture(
userId = 0L,
followedUserIdsFuture = mkEmptyFuture("followedUserIdsFuture"),
mutuallyFollowingUserIdsFuture = mkEmptyFuture("mutuallyFollowingUserIdsFuture"),
mutedUserIdsFuture = mkEmptyFuture("mutedUserIdsFuture"),
retweetsMutedUserIdsFuture = mkEmptyFuture("retweetsMutedUserIdsFuture")
)
}

View File

@ -0,0 +1,18 @@
package com.twitter.timelineranker.core
import com.twitter.search.common.constants.thriftscala.ThriftLanguage
import com.twitter.search.common.features.thriftscala.ThriftTweetFeatures
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelines.clients.gizmoduck.UserProfileInfo
import com.twitter.timelines.model.TweetId
import com.twitter.timelines.util.FutureUtils
import com.twitter.util.Future
case class HydratedCandidatesAndFeaturesEnvelope(
candidateEnvelope: CandidateEnvelope,
userLanguages: Seq[ThriftLanguage],
userProfileInfo: UserProfileInfo,
features: Map[TweetId, ThriftTweetFeatures] = Map.empty,
contentFeaturesFuture: Future[Map[TweetId, ContentFeatures]] = FutureUtils.EmptyMap,
tweetSourceTweetMap: Map[TweetId, TweetId] = Map.empty,
inReplyToTweetIds: Set[TweetId] = Set.empty)

View File

@ -0,0 +1,7 @@
package com.twitter.timelineranker.core
import com.twitter.timelines.model.tweet.HydratedTweet
case class HydratedTweets(
outerTweets: Seq[HydratedTweet],
innerTweets: Seq[HydratedTweet] = Seq.empty)

View File

@ -0,0 +1,13 @@
package com.twitter.timelineranker
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelines.configapi
package object core {
type FutureDependencyTransformer[-U, +V] = configapi.FutureDependencyTransformer[RecapQuery, U, V]
object FutureDependencyTransformer
extends configapi.FutureDependencyTransformerFunctions[RecapQuery]
type DependencyTransformer[-U, +V] = configapi.DependencyTransformer[RecapQuery, U, V]
object DependencyTransformer extends configapi.DependencyTransformerFunctions[RecapQuery]
}

View File

@ -0,0 +1,9 @@
scala_library(
sources = ["*.scala"],
platform = "java8",
tags = ["bazel-compatible"],
dependencies = [
"servo/decider",
"timelineranker/server/config",
],
)

View File

@ -0,0 +1,83 @@
package com.twitter.timelineranker.decider
import com.twitter.servo.decider.DeciderKeyEnum
object DeciderKey extends DeciderKeyEnum {
// Deciders that can be used to control load on TLR or its backends.
val EnableMaxConcurrencyLimiting: Value = Value("enable_max_concurrency_limiting")
// Deciders related to testing / debugging.
val EnableRoutingToRankerDevProxy: Value = Value("enable_routing_to_ranker_dev_proxy")
// Deciders related to authorization.
val ClientRequestAuthorization: Value = Value("client_request_authorization")
val ClientWriteWhitelist: Value = Value("client_write_whitelist")
val AllowTimelineMixerRecapProd: Value = Value("allow_timeline_mixer_recap_prod")
val AllowTimelineMixerRecycledProd: Value = Value("allow_timeline_mixer_recycled_prod")
val AllowTimelineMixerHydrateProd: Value = Value("allow_timeline_mixer_hydrate_prod")
val AllowTimelineMixerHydrateRecosProd: Value = Value("allow_timeline_mixer_hydrate_recos_prod")
val AllowTimelineMixerSeedAuthorsProd: Value = Value("allow_timeline_mixer_seed_authors_prod")
val AllowTimelineMixerSimclusterProd: Value = Value("allow_timeline_mixer_simcluster_prod")
val AllowTimelineMixerEntityTweetsProd: Value = Value("allow_timeline_mixer_entity_tweets_prod")
val AllowTimelineMixerListProd: Value = Value("allow_timeline_mixer_list_prod")
val AllowTimelineMixerListTweetProd: Value = Value("allow_timeline_mixer_list_tweet_prod")
val AllowTimelineMixerCommunityProd: Value = Value("allow_timeline_mixer_community_prod")
val AllowTimelineMixerCommunityTweetProd: Value = Value(
"allow_timeline_mixer_community_tweet_prod")
val AllowTimelineScorerRecommendedTrendTweetProd: Value = Value(
"allow_timeline_scorer_recommended_trend_tweet_prod")
val AllowTimelineMixerUtegLikedByTweetsProd: Value = Value(
"allow_timeline_mixer_uteg_liked_by_tweets_prod")
val AllowTimelineMixerStaging: Value = Value("allow_timeline_mixer_staging")
val AllowTimelineRankerProxy: Value = Value("allow_timeline_ranker_proxy")
val AllowTimelineRankerWarmup: Value = Value("allow_timeline_ranker_warmup")
val AllowTimelineScorerRecTopicTweetsProd: Value =
Value("allow_timeline_scorer_rec_topic_tweets_prod")
val AllowTimelineScorerPopularTopicTweetsProd: Value =
Value("allow_timeline_scorer_popular_topic_tweets_prod")
val AllowTimelineScorerHydrateTweetScoringProd: Value = Value(
"allow_timelinescorer_hydrate_tweet_scoring_prod")
val AllowTimelineServiceProd: Value = Value("allow_timeline_service_prod")
val AllowTimelineServiceStaging: Value = Value("allow_timeline_service_staging")
val RateLimitOverrideUnknown: Value = Value("rate_limit_override_unknown")
// Deciders related to reverse-chron home timeline materialization.
val MultiplierOfMaterializationTweetsFetched: Value = Value(
"multiplier_of_materialization_tweets_fetched"
)
val BackfillFilteredEntries: Value = Value("enable_backfill_filtered_entries")
val TweetsFilteringLossageThreshold: Value = Value("tweets_filtering_lossage_threshold")
val TweetsFilteringLossageLimit: Value = Value("tweets_filtering_lossage_limit")
val SupplementFollowsWithRealGraph: Value = Value("supplement_follows_with_real_graph")
// Deciders related to recap.
val RecapEnableContentFeaturesHydration: Value = Value("recap_enable_content_features_hydration")
val RecapMaxCountMultiplier: Value = Value("recap_max_count_multiplier")
val RecapEnableExtraSortingInResults: Value = Value("recap_enable_extra_sorting_in_results")
// Deciders related to recycled tweets.
val RecycledMaxCountMultiplier: Value = Value("recycled_max_count_multiplier")
val RecycledEnableContentFeaturesHydration: Value = Value(
"recycled_enable_content_features_hydration")
// Deciders related to entity tweets.
val EntityTweetsEnableContentFeaturesHydration: Value = Value(
"entity_tweets_enable_content_features_hydration")
// Deciders related to both recap and recycled tweets
val EnableRealGraphUsers: Value = Value("enable_real_graph_users")
val MaxRealGraphAndFollowedUsers: Value = Value("max_real_graph_and_followed_users")
// Deciders related to recap author
val RecapAuthorEnableNewPipeline: Value = Value("recap_author_enable_new_pipeline")
val RecapAuthorEnableContentFeaturesHydration: Value = Value(
"recap_author_enable_content_features_hydration")
// Deciders related to recap hydration (rectweet and ranked organic).
val RecapHydrationEnableContentFeaturesHydration: Value = Value(
"recap_hydration_enable_content_features_hydration")
// Deciders related to uteg liked by tweets
val UtegLikedByTweetsEnableContentFeaturesHydration: Value = Value(
"uteg_liked_by_tweets_enable_content_features_hydration")
}

View File

@ -0,0 +1,39 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/twitter/storehaus:core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider/src/main/scala/com/twitter/timelines/configapi/decider",
"finagle/finagle-core/src/main",
"servo/util/src/main/scala",
"src/thrift/com/twitter/search:earlybird-scala",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/common",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/config",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/core",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/entity_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/repository",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/util",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/visibility",
"timelines/src/main/scala/com/twitter/timelines/clients/gizmoduck",
"timelines/src/main/scala/com/twitter/timelines/clients/manhattan",
"timelines/src/main/scala/com/twitter/timelines/clients/relevance_search",
"timelines/src/main/scala/com/twitter/timelines/clients/tweetypie",
"timelines/src/main/scala/com/twitter/timelines/common/model",
"timelines/src/main/scala/com/twitter/timelines/config",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/options",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/utils",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/util/stats",
"timelines/src/main/scala/com/twitter/timelines/visibility",
"timelines/src/main/scala/com/twitter/timelines/visibility/model",
"util/util-core:util-core-util",
"util/util-core/src/main/scala/com/twitter/conversions",
"util/util-stats/src/main/scala",
],
)

View File

@ -0,0 +1,20 @@
package com.twitter.timelineranker.entity_tweets
import com.twitter.timelineranker.model.CandidateTweetsResult
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.util.Future
/**
* A repository of entity tweets candidates.
*
* For now, it does not cache any results therefore forwards all calls to the underlying source.
*/
class EntityTweetsRepository(source: EntityTweetsSource) {
def get(query: RecapQuery): Future[CandidateTweetsResult] = {
source.get(query)
}
def get(queries: Seq[RecapQuery]): Future[Seq[CandidateTweetsResult]] = {
source.get(queries)
}
}

View File

@ -0,0 +1,60 @@
package com.twitter.timelineranker.entity_tweets
import com.twitter.conversions.DurationOps._
import com.twitter.timelineranker.config.RequestScopes
import com.twitter.timelineranker.config.RuntimeConfiguration
import com.twitter.timelineranker.parameters.ConfigBuilder
import com.twitter.timelineranker.repository.CandidatesRepositoryBuilder
import com.twitter.timelineranker.visibility.SgsFollowGraphDataFields
import com.twitter.search.earlybird.thriftscala.EarlybirdService
import com.twitter.timelines.util.stats.RequestScope
import com.twitter.util.Duration
class EntityTweetsRepositoryBuilder(config: RuntimeConfiguration, configBuilder: ConfigBuilder)
extends CandidatesRepositoryBuilder(config) {
// Default client id for this repository if the upstream requests doesn't indicate one.
override val clientSubId = "community"
override val requestScope: RequestScope = RequestScopes.EntityTweetsSource
override val followGraphDataFieldsToFetch: SgsFollowGraphDataFields.ValueSet =
SgsFollowGraphDataFields.ValueSet(
SgsFollowGraphDataFields.FollowedUserIds,
SgsFollowGraphDataFields.MutuallyFollowingUserIds,
SgsFollowGraphDataFields.MutedUserIds
)
/**
* [1] timeout is derived from p9999 TLR <-> Earlybird latency and shall be less than
* request timeout of timelineranker client within downstream timelinemixer, which is
* 1s now
*
* [2] processing timeout is less than request timeout [1] with 100ms space for networking and
* other times such as gc
*/
override val searchProcessingTimeout: Duration = 550.milliseconds // [2]
override def earlybirdClient(scope: String): EarlybirdService.MethodPerEndpoint =
config.underlyingClients.createEarlybirdClient(
scope = scope,
requestTimeout = 650.milliseconds, // [1]
timeout = 900.milliseconds, // [1]
retryPolicy = config.underlyingClients.DefaultRetryPolicy
)
def apply(): EntityTweetsRepository = {
val entityTweetsSource = new EntityTweetsSource(
gizmoduckClient,
searchClient,
tweetyPieHighQoSClient,
userMetadataClient,
followGraphDataProvider,
clientFactories.visibilityEnforcerFactory.apply(
VisibilityRules,
RequestScopes.EntityTweetsSource
),
config.underlyingClients.contentFeaturesCache,
config.statsReceiver
)
new EntityTweetsRepository(entityTweetsSource)
}
}

View File

@ -0,0 +1,71 @@
package com.twitter.timelineranker.entity_tweets
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.TweetIdRange
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.clients.relevance_search.SearchClient.TweetTypes
import com.twitter.timelines.model.TweetId
import com.twitter.util.Future
object EntityTweetsSearchResultsTransform {
// If EntityTweetsQuery.maxCount is not specified, the following count is used.
val DefaultEntityTweetsMaxTweetCount = 200
}
/**
* Fetch entity tweets search results using the search client
* and populate them into the CandidateEnvelope
*/
class EntityTweetsSearchResultsTransform(
searchClient: SearchClient,
statsReceiver: StatsReceiver,
logSearchDebugInfo: Boolean = false)
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
import EntityTweetsSearchResultsTransform._
private[this] val maxCountStat = statsReceiver.stat("maxCount")
private[this] val numResultsFromSearchStat = statsReceiver.stat("numResultsFromSearch")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val maxCount = envelope.query.maxCount.getOrElse(DefaultEntityTweetsMaxTweetCount)
maxCountStat.add(maxCount)
val tweetIdRange = envelope.query.range
.map(TweetIdRange.fromTimelineRange)
.getOrElse(TweetIdRange.default)
val beforeTweetIdExclusive = tweetIdRange.toId
val afterTweetIdExclusive = tweetIdRange.fromId
val excludedTweetIds = envelope.query.excludedTweetIds.getOrElse(Seq.empty[TweetId]).toSet
val languages = envelope.query.languages.map(_.map(_.language))
envelope.followGraphData.inNetworkUserIdsFuture.flatMap { inNetworkUserIds =>
searchClient
.getEntityTweets(
userId = Some(envelope.query.userId),
followedUserIds = inNetworkUserIds.toSet,
maxCount = maxCount,
beforeTweetIdExclusive = beforeTweetIdExclusive,
afterTweetIdExclusive = afterTweetIdExclusive,
earlybirdOptions = envelope.query.earlybirdOptions,
semanticCoreIds = envelope.query.semanticCoreIds,
hashtags = envelope.query.hashtags,
languages = languages,
tweetTypes = TweetTypes.fromTweetKindOption(envelope.query.options),
searchOperator = envelope.query.searchOperator,
excludedTweetIds = excludedTweetIds,
logSearchDebugInfo = logSearchDebugInfo,
includeNullcastTweets = envelope.query.includeNullcastTweets.getOrElse(false),
includeTweetsFromArchiveIndex =
envelope.query.includeTweetsFromArchiveIndex.getOrElse(false),
authorIds = envelope.query.authorIds.map(_.toSet)
).map { results =>
numResultsFromSearchStat.add(results.size)
envelope.copy(searchResults = results)
}
}
}
}

View File

@ -0,0 +1,146 @@
package com.twitter.timelineranker.entity_tweets
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.storehaus.Store
import com.twitter.timelineranker.common._
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.model._
import com.twitter.timelineranker.parameters.entity_tweets.EntityTweetsParams._
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelineranker.util.CopyContentFeaturesIntoHydratedTweetsTransform
import com.twitter.timelineranker.util.CopyContentFeaturesIntoThriftTweetFeaturesTransform
import com.twitter.timelineranker.util.TweetFilters
import com.twitter.timelineranker.visibility.FollowGraphDataProvider
import com.twitter.timelines.clients.gizmoduck.GizmoduckClient
import com.twitter.timelines.clients.manhattan.UserMetadataClient
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.clients.tweetypie.TweetyPieClient
import com.twitter.timelines.model.TweetId
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.timelines.util.stats.RequestStatsReceiver
import com.twitter.timelines.visibility.VisibilityEnforcer
import com.twitter.util.Future
class EntityTweetsSource(
gizmoduckClient: GizmoduckClient,
searchClient: SearchClient,
tweetyPieClient: TweetyPieClient,
userMetadataClient: UserMetadataClient,
followGraphDataProvider: FollowGraphDataProvider,
visibilityEnforcer: VisibilityEnforcer,
contentFeaturesCache: Store[TweetId, ContentFeatures],
statsReceiver: StatsReceiver) {
private[this] val baseScope = statsReceiver.scope("entityTweetsSource")
private[this] val requestStats = RequestStatsReceiver(baseScope)
private[this] val failOpenScope = baseScope.scope("failOpen")
private[this] val userProfileHandler = new FailOpenHandler(failOpenScope, "userProfileInfo")
private[this] val userLanguagesHandler = new FailOpenHandler(failOpenScope, "userLanguages")
private[this] val followGraphDataTransform = new FollowGraphDataTransform(
followGraphDataProvider = followGraphDataProvider,
maxFollowedUsersProvider = DependencyProvider.from(MaxFollowedUsersParam)
)
private[this] val fetchSearchResultsTransform = new EntityTweetsSearchResultsTransform(
searchClient = searchClient,
statsReceiver = baseScope
)
private[this] val userProfileInfoTransform =
new UserProfileInfoTransform(userProfileHandler, gizmoduckClient)
private[this] val languagesTransform =
new UserLanguagesTransform(userLanguagesHandler, userMetadataClient)
private[this] val visibilityEnforcingTransform = new VisibilityEnforcingTransform(
visibilityEnforcer
)
private[this] val filters = TweetFilters.ValueSet(
TweetFilters.DuplicateTweets,
TweetFilters.DuplicateRetweets
)
private[this] val hydratedTweetsFilter = new HydratedTweetsFilterTransform(
outerFilters = filters,
innerFilters = TweetFilters.None,
useFollowGraphData = false,
useSourceTweets = false,
statsReceiver = baseScope,
numRetweetsAllowed = HydratedTweetsFilterTransform.NumDuplicateRetweetsAllowed
)
private[this] val contentFeaturesHydrationTransform =
new ContentFeaturesHydrationTransformBuilder(
tweetyPieClient = tweetyPieClient,
contentFeaturesCache = contentFeaturesCache,
enableContentFeaturesGate = RecapQuery.paramGate(EnableContentFeaturesHydrationParam),
enableTokensInContentFeaturesGate =
RecapQuery.paramGate(EnableTokensInContentFeaturesHydrationParam),
enableTweetTextInContentFeaturesGate =
RecapQuery.paramGate(EnableTweetTextInContentFeaturesHydrationParam),
enableConversationControlContentFeaturesGate =
RecapQuery.paramGate(EnableConversationControlInContentFeaturesHydrationParam),
enableTweetMediaHydrationGate = RecapQuery.paramGate(EnableTweetMediaHydrationParam),
hydrateInReplyToTweets = false,
statsReceiver = baseScope
).build()
private[this] def hydratesContentFeatures(
hydratedEnvelope: HydratedCandidatesAndFeaturesEnvelope
): Boolean =
hydratedEnvelope.candidateEnvelope.query.hydratesContentFeatures.getOrElse(true)
private[this] val contentFeaturesTransformer = FutureArrow.choose(
predicate = hydratesContentFeatures,
ifTrue = contentFeaturesHydrationTransform
.andThen(CopyContentFeaturesIntoThriftTweetFeaturesTransform)
.andThen(CopyContentFeaturesIntoHydratedTweetsTransform),
ifFalse = FutureArrow[
HydratedCandidatesAndFeaturesEnvelope,
HydratedCandidatesAndFeaturesEnvelope
](Future.value) // empty transformer
)
private[this] val candidateGenerationTransform = new CandidateGenerationTransform(baseScope)
private[this] val hydrationAndFilteringPipeline =
CreateCandidateEnvelopeTransform
.andThen(followGraphDataTransform) // Fetch follow graph data
.andThen(fetchSearchResultsTransform) // fetch search results
.andThen(SearchResultDedupAndSortingTransform) // dedup and order search results
.andThen(CandidateTweetHydrationTransform) // hydrate search results
.andThen(visibilityEnforcingTransform) // filter hydrated tweets to visible ones
.andThen(hydratedTweetsFilter) // filter hydrated tweets based on predefined filter
.andThen(
TrimToMatchHydratedTweetsTransform
) // trim search result set to match filtered hydrated tweets (this needs to be accurate for feature hydration)
// runs the main pipeline in parallel with fetching user profile info and user languages
private[this] val featureHydrationDataTransform =
new FeatureHydrationDataTransform(
hydrationAndFilteringPipeline,
languagesTransform,
userProfileInfoTransform
)
private[this] val tweetFeaturesHydrationTransform =
OutOfNetworkTweetsSearchFeaturesHydrationTransform
.andThen(contentFeaturesTransformer)
private[this] val featureHydrationPipeline =
featureHydrationDataTransform
.andThen(tweetFeaturesHydrationTransform)
.andThen(candidateGenerationTransform)
def get(query: RecapQuery): Future[CandidateTweetsResult] = {
requestStats.addEventStats {
featureHydrationPipeline(query)
}
}
def get(queries: Seq[RecapQuery]): Future[Seq[CandidateTweetsResult]] = {
Future.collect(queries.map(get))
}
}

View File

@ -0,0 +1,41 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/com/twitter/storehaus:core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider/src/main/scala/com/twitter/timelines/configapi/decider",
"finagle/finagle-core/src/main",
"servo/util/src/main/scala",
"src/thrift/com/twitter/search:earlybird-scala",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/common",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/config",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/core",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/monitoring",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/in_network_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/monitoring",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/recap/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/repository",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/util",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/visibility",
"timelines/src/main/scala/com/twitter/timelines/clients/gizmoduck",
"timelines/src/main/scala/com/twitter/timelines/clients/manhattan",
"timelines/src/main/scala/com/twitter/timelines/clients/relevance_search",
"timelines/src/main/scala/com/twitter/timelines/clients/tweetypie",
"timelines/src/main/scala/com/twitter/timelines/config",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/utils",
"timelines/src/main/scala/com/twitter/timelines/model/tweet",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelines/src/main/scala/com/twitter/timelines/util/stats",
"timelines/src/main/scala/com/twitter/timelines/visibility",
"timelines/src/main/scala/com/twitter/timelines/visibility/model",
"util/util-core:util-core-util",
"util/util-core/src/main/scala/com/twitter/conversions",
"util/util-stats/src/main/scala",
],
)

View File

@ -0,0 +1,31 @@
package com.twitter.timelineranker.in_network_tweets
import com.twitter.timelineranker.model.CandidateTweetsResult
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.parameters.in_network_tweets.InNetworkTweetParams
import com.twitter.util.Future
/**
* A repository of in-network tweet candidates.
* For now, it does not cache any results therefore forwards all calls to the underlying source.
*/
class InNetworkTweetRepository(
source: InNetworkTweetSource,
realtimeCGSource: InNetworkTweetSource) {
private[this] val enableRealtimeCGProvider =
DependencyProvider.from(InNetworkTweetParams.EnableEarlybirdRealtimeCgMigrationParam)
def get(query: RecapQuery): Future[CandidateTweetsResult] = {
if (enableRealtimeCGProvider(query)) {
realtimeCGSource.get(query)
} else {
source.get(query)
}
}
def get(queries: Seq[RecapQuery]): Future[Seq[CandidateTweetsResult]] = {
Future.collect(queries.map(query => get(query)))
}
}

View File

@ -0,0 +1,109 @@
package com.twitter.timelineranker.in_network_tweets
import com.twitter.conversions.DurationOps._
import com.twitter.finagle.service.RetryPolicy
import com.twitter.search.earlybird.thriftscala.EarlybirdService
import com.twitter.timelineranker.config.RequestScopes
import com.twitter.timelineranker.config.RuntimeConfiguration
import com.twitter.timelineranker.parameters.ConfigBuilder
import com.twitter.timelineranker.repository.CandidatesRepositoryBuilder
import com.twitter.timelineranker.visibility.SgsFollowGraphDataFields
import com.twitter.timelines.util.stats.RequestScope
import com.twitter.timelines.visibility.model.CheckedUserActorType
import com.twitter.timelines.visibility.model.ExclusionReason
import com.twitter.timelines.visibility.model.VisibilityCheckStatus
import com.twitter.timelines.visibility.model.VisibilityCheckUser
import com.twitter.util.Duration
object InNetworkTweetRepositoryBuilder {
val VisibilityRuleExclusions: Set[ExclusionReason] = Set[ExclusionReason](
ExclusionReason(
CheckedUserActorType(Some(false), VisibilityCheckUser.SourceUser),
Set(VisibilityCheckStatus.Blocked)
)
)
private val EarlybirdTimeout = 600.milliseconds
private val EarlybirdRequestTimeout = 600.milliseconds
/**
* The timeouts below are only used for the Earlybird Cluster Migration
*/
private val EarlybirdRealtimeCGTimeout = 600.milliseconds
private val EarlybirdRealtimeCGRequestTimeout = 600.milliseconds
}
class InNetworkTweetRepositoryBuilder(config: RuntimeConfiguration, configBuilder: ConfigBuilder)
extends CandidatesRepositoryBuilder(config) {
import InNetworkTweetRepositoryBuilder._
override val clientSubId = "recycled_tweets"
override val requestScope: RequestScope = RequestScopes.InNetworkTweetSource
override val followGraphDataFieldsToFetch: SgsFollowGraphDataFields.ValueSet =
SgsFollowGraphDataFields.ValueSet(
SgsFollowGraphDataFields.FollowedUserIds,
SgsFollowGraphDataFields.MutuallyFollowingUserIds,
SgsFollowGraphDataFields.MutedUserIds,
SgsFollowGraphDataFields.RetweetsMutedUserIds
)
override val searchProcessingTimeout: Duration = 200.milliseconds
override def earlybirdClient(scope: String): EarlybirdService.MethodPerEndpoint =
config.underlyingClients.createEarlybirdClient(
scope = scope,
requestTimeout = EarlybirdRequestTimeout,
timeout = EarlybirdTimeout,
retryPolicy = RetryPolicy.Never
)
private lazy val searchClientForSourceTweets =
newSearchClient(clientId = clientSubId + "_source_tweets")
/** The RealtimeCG clients below are only used for the Earlybird Cluster Migration */
private def earlybirdRealtimeCGClient(scope: String): EarlybirdService.MethodPerEndpoint =
config.underlyingClients.createEarlybirdRealtimeCgClient(
scope = scope,
requestTimeout = EarlybirdRealtimeCGRequestTimeout,
timeout = EarlybirdRealtimeCGTimeout,
retryPolicy = RetryPolicy.Never
)
private val realtimeCGClientSubId = "realtime_cg_recycled_tweets"
private lazy val searchRealtimeCGClient =
newSearchClient(earlybirdRealtimeCGClient, clientId = realtimeCGClientSubId)
def apply(): InNetworkTweetRepository = {
val inNetworkTweetSource = new InNetworkTweetSource(
gizmoduckClient,
searchClient,
searchClientForSourceTweets,
tweetyPieHighQoSClient,
userMetadataClient,
followGraphDataProvider,
config.underlyingClients.contentFeaturesCache,
clientFactories.visibilityEnforcerFactory.apply(
VisibilityRules,
RequestScopes.InNetworkTweetSource,
reasonsToExclude = InNetworkTweetRepositoryBuilder.VisibilityRuleExclusions
),
config.statsReceiver
)
val inNetworkTweetRealtimeCGSource = new InNetworkTweetSource(
gizmoduckClient,
searchRealtimeCGClient,
searchClientForSourceTweets, // do not migrate source_tweets as they are sharded by TweetID
tweetyPieHighQoSClient,
userMetadataClient,
followGraphDataProvider,
config.underlyingClients.contentFeaturesCache,
clientFactories.visibilityEnforcerFactory.apply(
VisibilityRules,
RequestScopes.InNetworkTweetSource,
reasonsToExclude = InNetworkTweetRepositoryBuilder.VisibilityRuleExclusions
),
config.statsReceiver.scope("replacementRealtimeCG")
)
new InNetworkTweetRepository(inNetworkTweetSource, inNetworkTweetRealtimeCGSource)
}
}

View File

@ -0,0 +1,271 @@
package com.twitter.timelineranker.in_network_tweets
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.util.FutureArrow
import com.twitter.storehaus.Store
import com.twitter.timelineranker.common._
import com.twitter.timelineranker.core.HydratedCandidatesAndFeaturesEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.timelineranker.model._
import com.twitter.timelineranker.monitoring.UsersSearchResultMonitoringTransform
import com.twitter.timelineranker.parameters.in_network_tweets.InNetworkTweetParams
import com.twitter.timelineranker.parameters.monitoring.MonitoringParams
import com.twitter.timelineranker.parameters.recap.RecapParams
import com.twitter.timelineranker.recap.model.ContentFeatures
import com.twitter.timelineranker.util.CopyContentFeaturesIntoHydratedTweetsTransform
import com.twitter.timelineranker.util.CopyContentFeaturesIntoThriftTweetFeaturesTransform
import com.twitter.timelineranker.util.TweetFilters
import com.twitter.timelineranker.visibility.FollowGraphDataProvider
import com.twitter.timelines.clients.gizmoduck.GizmoduckClient
import com.twitter.timelines.clients.manhattan.UserMetadataClient
import com.twitter.timelines.clients.relevance_search.SearchClient
import com.twitter.timelines.clients.tweetypie.TweetyPieClient
import com.twitter.timelines.model.TweetId
import com.twitter.timelines.util.FailOpenHandler
import com.twitter.timelines.util.stats.RequestStatsReceiver
import com.twitter.timelines.visibility.VisibilityEnforcer
import com.twitter.util.Future
class InNetworkTweetSource(
gizmoduckClient: GizmoduckClient,
searchClient: SearchClient,
searchClientForSourceTweets: SearchClient,
tweetyPieClient: TweetyPieClient,
userMetadataClient: UserMetadataClient,
followGraphDataProvider: FollowGraphDataProvider,
contentFeaturesCache: Store[TweetId, ContentFeatures],
visibilityEnforcer: VisibilityEnforcer,
statsReceiver: StatsReceiver) {
private[this] val baseScope = statsReceiver.scope("recycledTweetSource")
private[this] val requestStats = RequestStatsReceiver(baseScope)
private[this] val failOpenScope = baseScope.scope("failOpen")
private[this] val userProfileHandler = new FailOpenHandler(failOpenScope, "userProfileInfo")
private[this] val userLanguagesHandler = new FailOpenHandler(failOpenScope, "userLanguages")
private[this] val sourceTweetSearchHandler =
new FailOpenHandler(failOpenScope, "sourceTweetSearch")
private[this] val filters = TweetFilters.ValueSet(
TweetFilters.DuplicateTweets,
TweetFilters.DuplicateRetweets,
TweetFilters.TweetsFromNotFollowedUsers,
TweetFilters.NonReplyDirectedAtNotFollowedUsers
)
private[this] val hydrateReplyRootTweetProvider =
DependencyProvider.from(InNetworkTweetParams.EnableReplyRootTweetHydrationParam)
private[this] val sourceTweetsSearchResultsTransform = new SourceTweetsSearchResultsTransform(
searchClientForSourceTweets,
sourceTweetSearchHandler,
hydrateReplyRootTweetProvider = hydrateReplyRootTweetProvider,
perRequestSourceSearchClientIdProvider = DependencyProvider.None,
baseScope
)
private[this] val visibilityEnforcingTransform = new VisibilityEnforcingTransform(
visibilityEnforcer
)
private[this] val hydratedTweetsFilter = new HydratedTweetsFilterTransform(
outerFilters = filters,
innerFilters = TweetFilters.None,
useFollowGraphData = true,
useSourceTweets = true,
statsReceiver = baseScope,
numRetweetsAllowed = HydratedTweetsFilterTransform.NumDuplicateRetweetsAllowed
)
private[this] val dynamicHydratedTweetsFilter = new TweetKindOptionHydratedTweetsFilterTransform(
useFollowGraphData = true,
useSourceTweets = true,
statsReceiver = baseScope
)
private[this] val userProfileInfoTransform =
new UserProfileInfoTransform(userProfileHandler, gizmoduckClient)
private[this] val languagesTransform =
new UserLanguagesTransform(userLanguagesHandler, userMetadataClient)
private[this] def hydratesContentFeatures(
hydratedEnvelope: HydratedCandidatesAndFeaturesEnvelope
): Boolean =
hydratedEnvelope.candidateEnvelope.query.hydratesContentFeatures.getOrElse(true)
private[this] val contentFeaturesTransformer = FutureArrow.choose(
predicate = hydratesContentFeatures,
ifTrue = contentFeaturesHydrationTransform
.andThen(CopyContentFeaturesIntoThriftTweetFeaturesTransform)
.andThen(CopyContentFeaturesIntoHydratedTweetsTransform),
ifFalse = FutureArrow[
HydratedCandidatesAndFeaturesEnvelope,
HydratedCandidatesAndFeaturesEnvelope
](Future.value) // empty transformer
)
private[this] val contentFeaturesHydrationTransform =
new ContentFeaturesHydrationTransformBuilder(
tweetyPieClient = tweetyPieClient,
contentFeaturesCache = contentFeaturesCache,
enableContentFeaturesGate =
RecapQuery.paramGate(InNetworkTweetParams.EnableContentFeaturesHydrationParam),
enableTokensInContentFeaturesGate =
RecapQuery.paramGate(InNetworkTweetParams.EnableTokensInContentFeaturesHydrationParam),
enableTweetTextInContentFeaturesGate =
RecapQuery.paramGate(InNetworkTweetParams.EnableTweetTextInContentFeaturesHydrationParam),
enableConversationControlContentFeaturesGate = RecapQuery.paramGate(
InNetworkTweetParams.EnableConversationControlInContentFeaturesHydrationParam),
enableTweetMediaHydrationGate = RecapQuery.paramGate(
InNetworkTweetParams.EnableTweetMediaHydrationParam
),
hydrateInReplyToTweets = true,
statsReceiver = baseScope
).build()
private[this] val candidateGenerationTransform = new CandidateGenerationTransform(baseScope)
private[this] val maxFollowedUsersProvider =
DependencyProvider.from(InNetworkTweetParams.MaxFollowedUsersParam)
private[this] val earlybirdReturnAllResultsProvider =
DependencyProvider.from(InNetworkTweetParams.EnableEarlybirdReturnAllResultsParam)
private[this] val relevanceOptionsMaxHitsToProcessProvider =
DependencyProvider.from(InNetworkTweetParams.RelevanceOptionsMaxHitsToProcessParam)
private[this] val followGraphDataTransform =
new FollowGraphDataTransform(followGraphDataProvider, maxFollowedUsersProvider)
private[this] val enableRealGraphUsersProvider =
DependencyProvider.from(RecapParams.EnableRealGraphUsersParam)
private[this] val maxRealGraphAndFollowedUsersProvider =
DependencyProvider.from(RecapParams.MaxRealGraphAndFollowedUsersParam)
private[this] val maxRealGraphAndFollowedUsersFSOverrideProvider =
DependencyProvider.from(RecapParams.MaxRealGraphAndFollowedUsersFSOverrideParam)
private[this] val imputeRealGraphAuthorWeightsProvider =
DependencyProvider.from(RecapParams.ImputeRealGraphAuthorWeightsParam)
private[this] val imputeRealGraphAuthorWeightsPercentileProvider =
DependencyProvider.from(RecapParams.ImputeRealGraphAuthorWeightsPercentileParam)
private[this] val maxRealGraphAndFollowedUsersFromDeciderAndFS = DependencyProvider { envelope =>
maxRealGraphAndFollowedUsersFSOverrideProvider(envelope).getOrElse(
maxRealGraphAndFollowedUsersProvider(envelope))
}
private[this] val followAndRealGraphCombiningTransform = new FollowAndRealGraphCombiningTransform(
followGraphDataProvider = followGraphDataProvider,
maxFollowedUsersProvider = maxFollowedUsersProvider,
enableRealGraphUsersProvider = enableRealGraphUsersProvider,
maxRealGraphAndFollowedUsersProvider = maxRealGraphAndFollowedUsersFromDeciderAndFS,
imputeRealGraphAuthorWeightsProvider = imputeRealGraphAuthorWeightsProvider,
imputeRealGraphAuthorWeightsPercentileProvider = imputeRealGraphAuthorWeightsPercentileProvider,
statsReceiver = baseScope
)
private[this] val maxCountProvider = DependencyProvider { query =>
query.maxCount.getOrElse(query.params(InNetworkTweetParams.DefaultMaxTweetCount))
}
private[this] val maxCountWithMarginProvider = DependencyProvider { query =>
val maxCount = query.maxCount.getOrElse(query.params(InNetworkTweetParams.DefaultMaxTweetCount))
val multiplier = query.params(InNetworkTweetParams.MaxCountMultiplierParam)
(maxCount * multiplier).toInt
}
private[this] val debugAuthorsMonitoringProvider =
DependencyProvider.from(MonitoringParams.DebugAuthorsAllowListParam)
private[this] val retrieveSearchResultsTransform = new RecapSearchResultsTransform(
searchClient = searchClient,
maxCountProvider = maxCountWithMarginProvider,
returnAllResultsProvider = earlybirdReturnAllResultsProvider,
relevanceOptionsMaxHitsToProcessProvider = relevanceOptionsMaxHitsToProcessProvider,
enableExcludeSourceTweetIdsProvider = DependencyProvider.True,
enableSettingTweetTypesWithTweetKindOptionProvider =
DependencyProvider.from(RecapParams.EnableSettingTweetTypesWithTweetKindOption),
perRequestSearchClientIdProvider = DependencyProvider.None,
statsReceiver = baseScope,
logSearchDebugInfo = false
)
private[this] val preTruncateSearchResultsTransform =
new UsersSearchResultMonitoringTransform(
name = "RecapSearchResultsTruncationTransform",
new RecapSearchResultsTruncationTransform(
extraSortBeforeTruncationGate = DependencyProvider.True,
maxCountProvider = maxCountWithMarginProvider,
statsReceiver = baseScope.scope("afterSearchResultsTransform")
),
baseScope.scope("afterSearchResultsTransform"),
debugAuthorsMonitoringProvider
)
private[this] val finalTruncationTransform = new UsersSearchResultMonitoringTransform(
name = "RecapSearchResultsTruncationTransform",
new RecapSearchResultsTruncationTransform(
extraSortBeforeTruncationGate = DependencyProvider.True,
maxCountProvider = maxCountProvider,
statsReceiver = baseScope.scope("finalTruncation")
),
baseScope.scope("finalTruncation"),
debugAuthorsMonitoringProvider
)
// Fetch source tweets based on search results present in the envelope
// and hydrate them.
private[this] val fetchAndHydrateSourceTweets =
sourceTweetsSearchResultsTransform
.andThen(SourceTweetHydrationTransform)
// Hydrate candidate tweets and fetch source tweets in parallel
private[this] val hydrateTweetsAndSourceTweetsInParallel =
new HydrateTweetsAndSourceTweetsInParallelTransform(
candidateTweetHydration = CandidateTweetHydrationTransform,
sourceTweetHydration = fetchAndHydrateSourceTweets
)
private[this] val trimToMatchSearchResultsTransform = new TrimToMatchSearchResultsTransform(
hydrateReplyRootTweetProvider = hydrateReplyRootTweetProvider,
statsReceiver = baseScope
)
private[this] val hydrationAndFilteringPipeline =
CreateCandidateEnvelopeTransform // Create empty CandidateEnvelope
.andThen(followGraphDataTransform) // Fetch follow graph data
.andThen(followAndRealGraphCombiningTransform) // Experiment: expand seed author set
.andThen(retrieveSearchResultsTransform) // Fetch search results
.andThen(
preTruncateSearchResultsTransform
) // truncate the search result up to maxCount + some margin, preserving the random tweet
.andThen(SearchResultDedupAndSortingTransform) // dedups, and sorts reverse-chron
.andThen(hydrateTweetsAndSourceTweetsInParallel) // candidates + source tweets in parallel
.andThen(visibilityEnforcingTransform) // filter hydrated tweets to visible ones
.andThen(hydratedTweetsFilter) // filter hydrated tweets based on predefined filter
.andThen(dynamicHydratedTweetsFilter) // filter hydrated tweets based on query TweetKindOption
.andThen(TrimToMatchHydratedTweetsTransform) // trim searchResult to match with hydratedTweets
.andThen(
finalTruncationTransform
) // truncate the searchResult to exactly up to maxCount, preserving the random tweet
.andThen(
trimToMatchSearchResultsTransform
) // trim other fields to match with the final searchResult
// runs the main pipeline in parallel with fetching user profile info and user languages
private[this] val featureHydrationDataTransform = new FeatureHydrationDataTransform(
hydrationAndFilteringPipeline,
languagesTransform,
userProfileInfoTransform
)
private[this] val featureHydrationPipeline =
featureHydrationDataTransform
.andThen(InNetworkTweetsSearchFeaturesHydrationTransform)
.andThen(contentFeaturesTransformer)
.andThen(candidateGenerationTransform)
def get(query: RecapQuery): Future[CandidateTweetsResult] = {
requestStats.addEventStats {
featureHydrationPipeline(query)
}
}
def get(queries: Seq[RecapQuery]): Future[Seq[CandidateTweetsResult]] = {
Future.collect(queries.map(get))
}
}

View File

@ -0,0 +1,15 @@
scala_library(
sources = ["*.scala"],
platform = "java8",
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"3rdparty/jvm/org/apache/thrift:libthrift",
"scrooge/scrooge-core/src/main/scala",
"servo/util",
"src/thrift/com/twitter/search:earlybird-scala",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/core",
"util/util-stats/src/main/scala/com/twitter/finagle/stats",
],
)

View File

@ -0,0 +1,52 @@
package com.twitter.timelineranker.monitoring
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
import com.twitter.servo.util.FutureArrow
import com.twitter.timelineranker.core.CandidateEnvelope
import com.twitter.timelineranker.model.RecapQuery.DependencyProvider
import com.twitter.util.Future
/**
* Captures tweet counts pre and post transformation for a list of users
*/
class UsersSearchResultMonitoringTransform(
name: String,
underlyingTransformer: FutureArrow[CandidateEnvelope, CandidateEnvelope],
statsReceiver: StatsReceiver,
debugAuthorListDependencyProvider: DependencyProvider[Seq[Long]])
extends FutureArrow[CandidateEnvelope, CandidateEnvelope] {
private val scopedStatsReceiver = statsReceiver.scope(name)
private val preTransformCounter = (userId: Long) =>
scopedStatsReceiver
.scope("pre_transform").scope(userId.toString).counter("debug_author_list")
private val postTransformCounter = (userId: Long) =>
scopedStatsReceiver
.scope("post_transform").scope(userId.toString).counter("debug_author_list")
override def apply(envelope: CandidateEnvelope): Future[CandidateEnvelope] = {
val debugAuthorList = debugAuthorListDependencyProvider.apply(envelope.query)
envelope.searchResults
.filter(isTweetFromDebugAuthorList(_, debugAuthorList))
.flatMap(_.metadata)
.foreach(metadata => preTransformCounter(metadata.fromUserId).incr())
underlyingTransformer
.apply(envelope)
.map { result =>
envelope.searchResults
.filter(isTweetFromDebugAuthorList(_, debugAuthorList))
.flatMap(_.metadata)
.foreach(metadata => postTransformCounter(metadata.fromUserId).incr())
result
}
}
private def isTweetFromDebugAuthorList(
searchResult: ThriftSearchResult,
debugAuthorList: Seq[Long]
): Boolean =
searchResult.metadata.exists(metadata => debugAuthorList.contains(metadata.fromUserId))
}

View File

@ -0,0 +1,15 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"src/thrift/com/twitter/timelineranker/server/model:thrift-scala",
"timelineranker/common:model",
"timelines:observe",
"timelines/src/main/scala/com/twitter/timelines/authorization",
"timelines/src/main/scala/com/twitter/timelines/features",
"timelines/src/main/scala/com/twitter/timelines/model/types",
"util/util-core:util-core-util",
],
)

View File

@ -0,0 +1,36 @@
package com.twitter.timelineranker.observe
import com.twitter.servo.util.Gate
import com.twitter.timelineranker.model.TimelineQuery
import com.twitter.timelines.features.Features
import com.twitter.timelines.features.UserList
import com.twitter.timelines.observe.DebugObserver
import com.twitter.timelineranker.{thriftscala => thrift}
/**
* Builds the DebugObserver that is attached to thrift requests.
* This class exists to centralize the gates that determine whether or not
* to enable debug transcripts for a particular request.
*/
class DebugObserverBuilder(whitelist: UserList) {
lazy val observer: DebugObserver = build()
private[this] def build(): DebugObserver = {
new DebugObserver(queryGate)
}
private[observe] def queryGate: Gate[Any] = {
val shouldEnableDebug = whitelist.userIdGate(Features.DebugTranscript)
Gate { a: Any =>
a match {
case q: thrift.EngagedTweetsQuery => shouldEnableDebug(q.userId)
case q: thrift.RecapHydrationQuery => shouldEnableDebug(q.userId)
case q: thrift.RecapQuery => shouldEnableDebug(q.userId)
case q: TimelineQuery => shouldEnableDebug(q.userId)
case _ => false
}
}
}
}

View File

@ -0,0 +1,35 @@
package com.twitter.timelineranker.observe
import com.twitter.timelines.authorization.ReadRequest
import com.twitter.timelines.model.UserId
import com.twitter.timelines.observe.ObservedAndValidatedRequests
import com.twitter.timelines.observe.ServiceObserver
import com.twitter.timelines.observe.ServiceTracer
import com.twitter.util.Future
trait ObservedRequests extends ObservedAndValidatedRequests {
def observeAndValidate[R, Q](
request: Q,
viewerIds: Seq[UserId],
stats: ServiceObserver.Stats[Q],
exceptionHandler: PartialFunction[Throwable, Future[R]]
)(
f: Q => Future[R]
): Future[R] = {
super.observeAndValidate[Q, R](
request,
viewerIds,
ReadRequest,
validateRequest,
exceptionHandler,
stats,
ServiceTracer.identity[Q]
)(f)
}
def validateRequest[Q](request: Q): Unit = {
// TimelineQuery and its derived classes do not permit invalid instances to be constructed.
// Therefore no additional validation is required.
}
}

View File

@ -0,0 +1,18 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/entity_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/in_network_tweets",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/monitoring",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap_author",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap_hydration",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/revchron",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/uteg_liked_by_tweets",
],
)

View File

@ -0,0 +1,60 @@
package com.twitter.timelineranker.parameters
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.timelineranker.parameters.entity_tweets.EntityTweetsProduction
import com.twitter.timelineranker.parameters.recap.RecapProduction
import com.twitter.timelineranker.parameters.recap_author.RecapAuthorProduction
import com.twitter.timelineranker.parameters.recap_hydration.RecapHydrationProduction
import com.twitter.timelineranker.parameters.in_network_tweets.InNetworkTweetProduction
import com.twitter.timelineranker.parameters.revchron.ReverseChronProduction
import com.twitter.timelineranker.parameters.uteg_liked_by_tweets.UtegLikedByTweetsProduction
import com.twitter.timelineranker.parameters.monitoring.MonitoringProduction
import com.twitter.timelines.configapi.CompositeConfig
import com.twitter.timelines.configapi.Config
/**
* Builds global composite config containing prioritized "layers" of parameter overrides
* based on whitelists, experiments, and deciders. Generated config can be used in tests with
* mocked decider and whitelist.
*/
class ConfigBuilder(deciderGateBuilder: DeciderGateBuilder, statsReceiver: StatsReceiver) {
/**
* Production config which includes all configs which contribute to production behavior. At
* minimum, it should include all configs containing decider-based param overrides.
*
* It is important that the production config include all production param overrides as it is
* used to build holdback experiment configs; If the production config doesn't include all param
* overrides supporting production behavior then holdback experiment "production" buckets will
* not reflect production behavior.
*/
val prodConfig: Config = new CompositeConfig(
Seq(
new RecapProduction(deciderGateBuilder, statsReceiver).config,
new InNetworkTweetProduction(deciderGateBuilder).config,
new ReverseChronProduction(deciderGateBuilder).config,
new EntityTweetsProduction(deciderGateBuilder).config,
new RecapAuthorProduction(deciderGateBuilder).config,
new RecapHydrationProduction(deciderGateBuilder).config,
new UtegLikedByTweetsProduction(deciderGateBuilder).config,
MonitoringProduction.config
),
"prodConfig"
)
val whitelistConfig: Config = new CompositeConfig(
Seq(
// No whitelists configured at present.
),
"whitelistConfig"
)
val rootConfig: Config = new CompositeConfig(
Seq(
whitelistConfig,
prodConfig
),
"rootConfig"
)
}

View File

@ -0,0 +1,12 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider/src/main/scala/com/twitter/timelines/configapi/decider",
"servo/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
],
)

View File

@ -0,0 +1,65 @@
package com.twitter.timelineranker.parameters.entity_tweets
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelines.configapi.decider.DeciderParam
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
object EntityTweetsParams {
/**
* Controls limit on the number of followed users fetched from SGS.
*/
object MaxFollowedUsersParam
extends FSBoundedParam[Int](
name = "entity_tweets_max_followed_users",
default = 1000,
min = 0,
max = 5000
)
/**
* Enables semantic core, penguin, and tweetypie content features in entity tweets source.
*/
object EnableContentFeaturesHydrationParam
extends DeciderParam[Boolean](
decider = DeciderKey.EntityTweetsEnableContentFeaturesHydration,
default = false
)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "entity_tweets_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "entity_tweets_enable_tweet_text_in_content_features_hydration",
default = false
)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_entity_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_entity_tweets_enable",
default = false
)
}

View File

@ -0,0 +1,42 @@
package com.twitter.timelineranker.parameters.entity_tweets
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.entity_tweets.EntityTweetsParams._
import com.twitter.timelines.configapi.decider.DeciderUtils
import com.twitter.timelines.configapi.BaseConfig
import com.twitter.timelines.configapi.BaseConfigBuilder
import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil
import com.twitter.timelines.configapi.Param
object EntityTweetsProduction {
val deciderByParam: Map[Param[_], DeciderKeyName] = Map[Param[_], DeciderKeyName](
EnableContentFeaturesHydrationParam -> DeciderKey.EntityTweetsEnableContentFeaturesHydration
)
}
case class EntityTweetsProduction(deciderGateBuilder: DeciderGateBuilder) {
val booleanDeciderOverrides = DeciderUtils.getBooleanDeciderOverrides(
deciderGateBuilder,
EnableContentFeaturesHydrationParam
)
val booleanFeatureSwitchOverrides = FeatureSwitchOverrideUtil.getBooleanFSOverrides(
EnableTokensInContentFeaturesHydrationParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableConversationControlInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam
)
val intFeatureSwitchOverrides = FeatureSwitchOverrideUtil.getBoundedIntFSOverrides(
MaxFollowedUsersParam
)
val config: BaseConfig = new BaseConfigBuilder()
.set(booleanDeciderOverrides: _*)
.set(booleanFeatureSwitchOverrides: _*)
.set(intFeatureSwitchOverrides: _*)
.build()
}

View File

@ -0,0 +1,15 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider/src/main/scala/com/twitter/timelines/configapi/decider",
"servo/decider",
"servo/util/src/main/scala",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/recap",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
],
)

View File

@ -0,0 +1,133 @@
package com.twitter.timelineranker.parameters.in_network_tweets
import com.twitter.timelineranker.parameters.recap.RecapQueryContext
import com.twitter.timelines.configapi.decider._
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.Param
object InNetworkTweetParams {
import RecapQueryContext._
/**
* Controls limit on the number of followed users fetched from SGS.
*
* The specific default value below is for blender-timelines parity.
*/
object MaxFollowedUsersParam
extends FSBoundedParam[Int](
name = "recycled_max_followed_users",
default = MaxFollowedUsers.default,
min = MaxFollowedUsers.bounds.minInclusive,
max = MaxFollowedUsers.bounds.maxInclusive
)
/**
* Controls limit on the number of hits for Earlybird.
*
*/
object RelevanceOptionsMaxHitsToProcessParam
extends FSBoundedParam[Int](
name = "recycled_relevance_options_max_hits_to_process",
default = 500,
min = 100,
max = 20000
)
/**
* Fallback value for maximum number of search results, if not specified by query.maxCount
*/
object DefaultMaxTweetCount extends Param(200)
/**
* We multiply maxCount (caller supplied value) by this multiplier and fetch those many
* candidates from search so that we are left with sufficient number of candidates after
* hydration and filtering.
*/
object MaxCountMultiplierParam
extends Param(MaxCountMultiplier.default)
with DeciderValueConverter[Double] {
override def convert: IntConverter[Double] =
OutputBoundIntConverter[Double](divideDeciderBy100 _, MaxCountMultiplier.bounds)
}
/**
* Enable [[SearchQueryBuilder.createExcludedSourceTweetIdsQuery]]
*/
object EnableExcludeSourceTweetIdsQueryParam
extends FSParam[Boolean](
name = "recycled_exclude_source_tweet_ids_query_enable",
default = false
)
object EnableEarlybirdReturnAllResultsParam
extends FSParam[Boolean](
name = "recycled_enable_earlybird_return_all_results",
default = true
)
/**
* FS-controlled param to enable anti-dilution transform for DDG-16198
*/
object RecycledMaxFollowedUsersEnableAntiDilutionParam
extends FSParam[Boolean](
name = "recycled_max_followed_users_enable_anti_dilution",
default = false
)
/**
* Enables semantic core, penguin, and tweetypie content features in recycled source.
*/
object EnableContentFeaturesHydrationParam extends Param(default = true)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "recycled_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "recycled_enable_tweet_text_in_content_features_hydration",
default = false
)
/**
* Enables hydrating root tweet of in-network replies and extended replies
*/
object EnableReplyRootTweetHydrationParam
extends FSParam(
name = "recycled_enable_reply_root_tweet_hydration",
default = true
)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_recycled_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_recycled_enable",
default = false
)
object EnableEarlybirdRealtimeCgMigrationParam
extends FSParam(
name = "recycled_enable_earlybird_realtime_cg_migration",
default = false
)
}

View File

@ -0,0 +1,71 @@
package com.twitter.timelineranker.parameters.in_network_tweets
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.in_network_tweets.InNetworkTweetParams._
import com.twitter.timelineranker.parameters.util.ConfigHelper
import com.twitter.timelines.configapi._
import com.twitter.servo.decider.DeciderKeyEnum
object InNetworkTweetProduction {
val deciderByParam: Map[Param[_], DeciderKeyEnum#Value] = Map[Param[_], DeciderKeyName](
EnableContentFeaturesHydrationParam -> DeciderKey.RecycledEnableContentFeaturesHydration,
MaxCountMultiplierParam -> DeciderKey.RecycledMaxCountMultiplier
)
val doubleParams: Seq[MaxCountMultiplierParam.type] = Seq(
MaxCountMultiplierParam
)
val booleanDeciderParams: Seq[EnableContentFeaturesHydrationParam.type] = Seq(
EnableContentFeaturesHydrationParam
)
val booleanFeatureSwitchParams: Seq[FSParam[Boolean]] = Seq(
EnableExcludeSourceTweetIdsQueryParam,
EnableTokensInContentFeaturesHydrationParam,
EnableReplyRootTweetHydrationParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableConversationControlInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam,
EnableEarlybirdReturnAllResultsParam,
EnableEarlybirdRealtimeCgMigrationParam,
RecycledMaxFollowedUsersEnableAntiDilutionParam
)
val boundedIntFeatureSwitchParams: Seq[FSBoundedParam[Int]] = Seq(
MaxFollowedUsersParam,
RelevanceOptionsMaxHitsToProcessParam
)
}
class InNetworkTweetProduction(deciderGateBuilder: DeciderGateBuilder) {
val configHelper: ConfigHelper =
new ConfigHelper(InNetworkTweetProduction.deciderByParam, deciderGateBuilder)
val doubleDeciderOverrides: Seq[OptionalOverride[Double]] =
configHelper.createDeciderBasedOverrides(InNetworkTweetProduction.doubleParams)
val booleanDeciderOverrides: Seq[OptionalOverride[Boolean]] =
configHelper.createDeciderBasedBooleanOverrides(InNetworkTweetProduction.booleanDeciderParams)
val boundedIntFeatureSwitchOverrides: Seq[OptionalOverride[Int]] =
FeatureSwitchOverrideUtil.getBoundedIntFSOverrides(
InNetworkTweetProduction.boundedIntFeatureSwitchParams: _*)
val booleanFeatureSwitchOverrides: Seq[OptionalOverride[Boolean]] =
FeatureSwitchOverrideUtil.getBooleanFSOverrides(
InNetworkTweetProduction.booleanFeatureSwitchParams: _*)
val config: BaseConfig = new BaseConfigBuilder()
.set(
booleanDeciderOverrides: _*
)
.set(
doubleDeciderOverrides: _*
)
.set(
boundedIntFeatureSwitchOverrides: _*
)
.set(
booleanFeatureSwitchOverrides: _*
)
.build(InNetworkTweetProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,11 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider",
"servo/util/src/main/scala",
],
)

View File

@ -0,0 +1,13 @@
package com.twitter.timelineranker.parameters.monitoring
import com.twitter.timelines.configapi.FSParam
object MonitoringParams {
object DebugAuthorsAllowListParam
extends FSParam[Seq[Long]](
name = "monitoring_debug_authors_allow_list",
default = Seq.empty[Long]
)
}

View File

@ -0,0 +1,14 @@
package com.twitter.timelineranker.parameters.monitoring
import com.twitter.timelines.configapi.BaseConfigBuilder
import com.twitter.timelineranker.parameters.monitoring.MonitoringParams.DebugAuthorsAllowListParam
import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil
object MonitoringProduction {
private val longSeqOverrides =
FeatureSwitchOverrideUtil.getLongSeqFSOverrides(DebugAuthorsAllowListParam)
val config = BaseConfigBuilder()
.set(longSeqOverrides: _*)
.build()
}

View File

@ -0,0 +1,18 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider/src/main/scala/com/twitter/timelines/configapi/decider",
"servo/decider",
"servo/util/src/main/scala",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
"timelines/src/main/scala/com/twitter/timelines/experiment",
"util/util-logging",
"util/util-stats",
],
)

View File

@ -0,0 +1,231 @@
package com.twitter.timelineranker.parameters.recap
import com.twitter.timelines.configapi.decider._
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.Param
import com.twitter.timelines.util.bounds.BoundsWithDefault
object RecapParams {
val MaxFollowedUsers: BoundsWithDefault[Int] = BoundsWithDefault[Int](1, 3000, 1000)
val MaxCountMultiplier: BoundsWithDefault[Double] = BoundsWithDefault[Double](0.1, 2.0, 2.0)
val MaxRealGraphAndFollowedUsers: BoundsWithDefault[Int] = BoundsWithDefault[Int](0, 2000, 1000)
val ProbabilityRandomTweet: BoundsWithDefault[Double] = BoundsWithDefault[Double](0.0, 1.0, 0.0)
/**
* Controls limit on the number of followed users fetched from SGS.
*
* The specific default value below is for blender-timelines parity.
*/
object MaxFollowedUsersParam
extends FSBoundedParam[Int](
name = "recap_max_followed_users",
default = MaxFollowedUsers.default,
min = MaxFollowedUsers.bounds.minInclusive,
max = MaxFollowedUsers.bounds.maxInclusive
)
/**
* Controls limit on the number of hits for Earlybird.
* We added it solely for backward compatibility, to align with recycled.
* RecapSource is deprecated, but, this param is used by RecapAuthor source
*/
object RelevanceOptionsMaxHitsToProcessParam
extends FSBoundedParam[Int](
name = "recap_relevance_options_max_hits_to_process",
default = 500,
min = 100,
max = 20000
)
/**
* Enables fetching author seedset from real graph users. Only used if user follows >= 1000.
* If true, expands author seedset with real graph users and recent followed users.
* Otherwise, user seedset only includes followed users.
*/
object EnableRealGraphUsersParam extends Param(false)
/**
* Only used if EnableRealGraphUsersParam is true and OnlyRealGraphUsersParam is false.
* Maximum number of real graph users and recent followed users when mixing recent/real-graph users.
*/
object MaxRealGraphAndFollowedUsersParam
extends Param(MaxRealGraphAndFollowedUsers.default)
with DeciderValueConverter[Int] {
override def convert: IntConverter[Int] =
OutputBoundIntConverter(MaxRealGraphAndFollowedUsers.bounds)
}
/**
* FS-controlled param to override the MaxRealGraphAndFollowedUsersParam decider value for experiments
*/
object MaxRealGraphAndFollowedUsersFSOverrideParam
extends FSBoundedParam[Option[Int]](
name = "max_real_graph_and_followers_users_fs_override_param",
default = None,
min = Some(100),
max = Some(10000)
)
/**
* Experimental params for leveling the playing field between user folowees received from
* real-graph and follow-graph stores.
* Author relevance scores returned by real-graph are currently being used for light-ranking
* in-network tweet candidates.
* Follow-graph store returns the most recent followees without any relevance scores
* We are trying to impute the missing scores by using aggregated statistics (min, avg, p50, etc.)
* of real-graph scores.
*/
object ImputeRealGraphAuthorWeightsParam
extends FSParam(name = "impute_real_graph_author_weights", default = false)
object ImputeRealGraphAuthorWeightsPercentileParam
extends FSBoundedParam[Int](
name = "impute_real_graph_author_weights_percentile",
default = 50,
min = 0,
max = 99)
/**
* Enable running the new pipeline for recap author source
*/
object EnableNewRecapAuthorPipeline extends Param(false)
/**
* Fallback value for maximum number of search results, if not specified by query.maxCount
*/
object DefaultMaxTweetCount extends Param(200)
/**
* We multiply maxCount (caller supplied value) by this multiplier and fetch those many
* candidates from search so that we are left with sufficient number of candidates after
* hydration and filtering.
*/
object MaxCountMultiplierParam
extends Param(MaxCountMultiplier.default)
with DeciderValueConverter[Double] {
override def convert: IntConverter[Double] =
OutputBoundIntConverter[Double](divideDeciderBy100 _, MaxCountMultiplier.bounds)
}
/**
* Enables return all results from search index.
*/
object EnableReturnAllResultsParam
extends FSParam(name = "recap_enable_return_all_results", default = false)
/**
* Includes one or multiple random tweets in the response.
*/
object IncludeRandomTweetParam
extends FSParam(name = "recap_include_random_tweet", default = false)
/**
* One single random tweet (true) or tag tweet as random with given probability (false).
*/
object IncludeSingleRandomTweetParam
extends FSParam(name = "recap_include_random_tweet_single", default = true)
/**
* Probability to tag a tweet as random (will not be ranked).
*/
object ProbabilityRandomTweetParam
extends FSBoundedParam(
name = "recap_include_random_tweet_probability",
default = ProbabilityRandomTweet.default,
min = ProbabilityRandomTweet.bounds.minInclusive,
max = ProbabilityRandomTweet.bounds.maxInclusive)
/**
* Enable extra sorting by score for search results.
*/
object EnableExtraSortingInSearchResultParam extends Param(true)
/**
* Enables semantic core, penguin, and tweetypie content features in recap source.
*/
object EnableContentFeaturesHydrationParam extends Param(true)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "recap_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "recap_enable_tweet_text_in_content_features_hydration",
default = false
)
/**
* Enables hydrating in-network inReplyToTweet features
*/
object EnableInNetworkInReplyToTweetFeaturesHydrationParam
extends FSParam(
name = "recap_enable_in_network_in_reply_to_tweet_features_hydration",
default = false
)
/**
* Enables hydrating root tweet of in-network replies and extended replies
*/
object EnableReplyRootTweetHydrationParam
extends FSParam(
name = "recap_enable_reply_root_tweet_hydration",
default = false
)
/**
* Enable setting tweetTypes in search queries with TweetKindOption in RecapQuery
*/
object EnableSettingTweetTypesWithTweetKindOption
extends FSParam(
name = "recap_enable_setting_tweet_types_with_tweet_kind_option",
default = false
)
/**
* Enable relevance search, otherwise recency search from earlybird.
*/
object EnableRelevanceSearchParam
extends FSParam(
name = "recap_enable_relevance_search",
default = true
)
object EnableExpandedExtendedRepliesFilterParam
extends FSParam(
name = "recap_enable_expanded_extended_replies_filter",
default = false
)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_recap_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_recap_enable",
default = false
)
object EnableExcludeSourceTweetIdsQueryParam
extends FSParam[Boolean](
name = "recap_exclude_source_tweet_ids_query_enable",
default = false
)
}

View File

@ -0,0 +1,115 @@
package com.twitter.timelineranker.parameters.recap
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.recap.RecapParams._
import com.twitter.timelineranker.parameters.util.ConfigHelper
import com.twitter.timelines.configapi._
import com.twitter.servo.decider.DeciderKeyEnum
object RecapProduction {
val deciderByParam: Map[Param[_], DeciderKeyEnum#Value] = Map[Param[_], DeciderKeyName](
EnableRealGraphUsersParam -> DeciderKey.EnableRealGraphUsers,
MaxRealGraphAndFollowedUsersParam -> DeciderKey.MaxRealGraphAndFollowedUsers,
EnableContentFeaturesHydrationParam -> DeciderKey.RecapEnableContentFeaturesHydration,
MaxCountMultiplierParam -> DeciderKey.RecapMaxCountMultiplier,
EnableNewRecapAuthorPipeline -> DeciderKey.RecapAuthorEnableNewPipeline,
RecapParams.EnableExtraSortingInSearchResultParam -> DeciderKey.RecapEnableExtraSortingInResults
)
val intParams: Seq[MaxRealGraphAndFollowedUsersParam.type] = Seq(
MaxRealGraphAndFollowedUsersParam
)
val doubleParams: Seq[MaxCountMultiplierParam.type] = Seq(
MaxCountMultiplierParam
)
val boundedDoubleFeatureSwitchParams: Seq[FSBoundedParam[Double]] = Seq(
RecapParams.ProbabilityRandomTweetParam
)
val booleanParams: Seq[Param[Boolean]] = Seq(
EnableRealGraphUsersParam,
EnableContentFeaturesHydrationParam,
EnableNewRecapAuthorPipeline,
RecapParams.EnableExtraSortingInSearchResultParam
)
val booleanFeatureSwitchParams: Seq[FSParam[Boolean]] = Seq(
RecapParams.EnableReturnAllResultsParam,
RecapParams.IncludeRandomTweetParam,
RecapParams.IncludeSingleRandomTweetParam,
RecapParams.EnableInNetworkInReplyToTweetFeaturesHydrationParam,
RecapParams.EnableReplyRootTweetHydrationParam,
RecapParams.EnableSettingTweetTypesWithTweetKindOption,
RecapParams.EnableRelevanceSearchParam,
EnableTokensInContentFeaturesHydrationParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableExpandedExtendedRepliesFilterParam,
EnableConversationControlInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam,
ImputeRealGraphAuthorWeightsParam,
EnableExcludeSourceTweetIdsQueryParam
)
val boundedIntFeatureSwitchParams: Seq[FSBoundedParam[Int]] = Seq(
RecapParams.MaxFollowedUsersParam,
ImputeRealGraphAuthorWeightsPercentileParam,
RecapParams.RelevanceOptionsMaxHitsToProcessParam
)
}
class RecapProduction(deciderGateBuilder: DeciderGateBuilder, statsReceiver: StatsReceiver) {
val configHelper: ConfigHelper =
new ConfigHelper(RecapProduction.deciderByParam, deciderGateBuilder)
val intOverrides: Seq[OptionalOverride[Int]] =
configHelper.createDeciderBasedOverrides(RecapProduction.intParams)
val optionalBoundedIntFeatureSwitchOverrides: Seq[OptionalOverride[Option[Int]]] =
FeatureSwitchOverrideUtil.getBoundedOptionalIntOverrides(
(
MaxRealGraphAndFollowedUsersFSOverrideParam,
"max_real_graph_and_followers_users_fs_override_defined",
"max_real_graph_and_followers_users_fs_override_value"
)
)
val doubleOverrides: Seq[OptionalOverride[Double]] =
configHelper.createDeciderBasedOverrides(RecapProduction.doubleParams)
val booleanOverrides: Seq[OptionalOverride[Boolean]] =
configHelper.createDeciderBasedBooleanOverrides(RecapProduction.booleanParams)
val booleanFeatureSwitchOverrides: Seq[OptionalOverride[Boolean]] =
FeatureSwitchOverrideUtil.getBooleanFSOverrides(RecapProduction.booleanFeatureSwitchParams: _*)
val boundedDoubleFeatureSwitchOverrides: Seq[OptionalOverride[Double]] =
FeatureSwitchOverrideUtil.getBoundedDoubleFSOverrides(
RecapProduction.boundedDoubleFeatureSwitchParams: _*)
val boundedIntFeatureSwitchOverrides: Seq[OptionalOverride[Int]] =
FeatureSwitchOverrideUtil.getBoundedIntFSOverrides(
RecapProduction.boundedIntFeatureSwitchParams: _*)
val config: BaseConfig = new BaseConfigBuilder()
.set(
intOverrides: _*
)
.set(
booleanOverrides: _*
)
.set(
doubleOverrides: _*
)
.set(
booleanFeatureSwitchOverrides: _*
)
.set(
boundedIntFeatureSwitchOverrides: _*
)
.set(
optionalBoundedIntFeatureSwitchOverrides: _*
)
.set(
boundedDoubleFeatureSwitchOverrides: _*
)
.build(RecapProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,79 @@
package com.twitter.timelineranker.parameters.recap
import com.twitter.timelineranker.model.RecapQuery
import com.twitter.timelines.util.bounds.BoundsWithDefault
object RecapQueryContext {
val MaxFollowedUsers: BoundsWithDefault[Int] = BoundsWithDefault[Int](1, 3000, 1000)
val MaxCountMultiplier: BoundsWithDefault[Double] = BoundsWithDefault[Double](0.1, 2.0, 2.0)
val MaxRealGraphAndFollowedUsers: BoundsWithDefault[Int] = BoundsWithDefault[Int](0, 2000, 1000)
def getDefaultContext(query: RecapQuery): RecapQueryContext = {
new RecapQueryContextImpl(
query,
getEnableHydrationUsingTweetyPie = () => false,
getMaxFollowedUsers = () => MaxFollowedUsers.default,
getMaxCountMultiplier = () => MaxCountMultiplier.default,
getEnableRealGraphUsers = () => false,
getOnlyRealGraphUsers = () => false,
getMaxRealGraphAndFollowedUsers = () => MaxRealGraphAndFollowedUsers.default,
getEnableTextFeatureHydration = () => false
)
}
}
// Note that methods that return parameter value always use () to indicate that
// side effects may be involved in their invocation.
trait RecapQueryContext {
def query: RecapQuery
// If true, tweet hydration are performed by calling TweetyPie.
// Otherwise, tweets are partially hydrated based on information in ThriftSearchResult.
def enableHydrationUsingTweetyPie(): Boolean
// Maximum number of followed user accounts to use when fetching recap tweets.
def maxFollowedUsers(): Int
// We multiply maxCount (caller supplied value) by this multiplier and fetch those many
// candidates from search so that we are left with sufficient number of candidates after
// hydration and filtering.
def maxCountMultiplier(): Double
// Only used if user follows >= 1000.
// If true, fetches recap/recycled tweets using author seedset mixing with real graph users and followed users.
// Otherwise, fetches recap/recycled tweets only using followed users
def enableRealGraphUsers(): Boolean
// Only used if enableRealGraphUsers is true.
// If true, user seedset only contains real graph users.
// Otherwise, user seedset contains real graph users and recent followed users.
def onlyRealGraphUsers(): Boolean
// Only used if enableRealGraphUsers is true and onlyRealGraphUsers is false.
// Maximum number of real graph users and recent followed users when mixing recent/real-graph users.
def maxRealGraphAndFollowedUsers(): Int
// If true, text features are hydrated for prediction.
// Otherwise those feature values are not set at all.
def enableTextFeatureHydration(): Boolean
}
class RecapQueryContextImpl(
override val query: RecapQuery,
getEnableHydrationUsingTweetyPie: () => Boolean,
getMaxFollowedUsers: () => Int,
getMaxCountMultiplier: () => Double,
getEnableRealGraphUsers: () => Boolean,
getOnlyRealGraphUsers: () => Boolean,
getMaxRealGraphAndFollowedUsers: () => Int,
getEnableTextFeatureHydration: () => Boolean)
extends RecapQueryContext {
override def enableHydrationUsingTweetyPie(): Boolean = { getEnableHydrationUsingTweetyPie() }
override def maxFollowedUsers(): Int = { getMaxFollowedUsers() }
override def maxCountMultiplier(): Double = { getMaxCountMultiplier() }
override def enableRealGraphUsers(): Boolean = { getEnableRealGraphUsers() }
override def onlyRealGraphUsers(): Boolean = { getOnlyRealGraphUsers() }
override def maxRealGraphAndFollowedUsers(): Int = { getMaxRealGraphAndFollowedUsers() }
override def enableTextFeatureHydration(): Boolean = { getEnableTextFeatureHydration() }
}

View File

@ -0,0 +1,12 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
],
)

View File

@ -0,0 +1,53 @@
package com.twitter.timelineranker.parameters.recap_author
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.Param
object RecapAuthorParams {
/**
* Enables semantic core, penguin, and tweetypie content features in recap author source.
*/
object EnableContentFeaturesHydrationParam extends Param(false)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "recap_author_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "recap_author_enable_tweet_text_in_content_features_hydration",
default = false
)
object EnableEarlybirdRealtimeCgMigrationParam
extends FSParam(
name = "recap_author_enable_earlybird_realtime_cg_migration",
default = false
)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_recap_author_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_recap_author_enable",
default = false
)
}

View File

@ -0,0 +1,46 @@
package com.twitter.timelineranker.parameters.recap_author
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.recap_author.RecapAuthorParams._
import com.twitter.timelineranker.parameters.util.ConfigHelper
import com.twitter.timelines.configapi._
object RecapAuthorProduction {
val deciderByParam: Map[Param[_], DeciderKeyName] = Map[Param[_], DeciderKeyName](
EnableContentFeaturesHydrationParam -> DeciderKey.RecapAuthorEnableContentFeaturesHydration
)
val booleanParams: Seq[EnableContentFeaturesHydrationParam.type] = Seq(
EnableContentFeaturesHydrationParam
)
val booleanFeatureSwitchParams: Seq[FSParam[Boolean]] = Seq(
EnableTokensInContentFeaturesHydrationParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableConversationControlInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam,
EnableEarlybirdRealtimeCgMigrationParam
)
}
class RecapAuthorProduction(deciderGateBuilder: DeciderGateBuilder) {
val configHelper: ConfigHelper =
new ConfigHelper(RecapAuthorProduction.deciderByParam, deciderGateBuilder)
val booleanOverrides: Seq[OptionalOverride[Boolean]] =
configHelper.createDeciderBasedBooleanOverrides(RecapAuthorProduction.booleanParams)
val booleanFeatureSwitchOverrides: Seq[OptionalOverride[Boolean]] =
FeatureSwitchOverrideUtil.getBooleanFSOverrides(
RecapAuthorProduction.booleanFeatureSwitchParams: _*
)
val config: BaseConfig = new BaseConfigBuilder()
.set(
booleanOverrides: _*
).set(
booleanFeatureSwitchOverrides: _*
)
.build(RecapAuthorProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,12 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
],
)

View File

@ -0,0 +1,48 @@
package com.twitter.timelineranker.parameters.recap_hydration
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.Param
object RecapHydrationParams {
/**
* Enables semantic core, penguin, and tweetypie content features in recap hydration source.
*/
object EnableContentFeaturesHydrationParam extends Param(false)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "recap_hydration_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "recap_hydration_enable_tweet_text_in_content_features_hydration",
default = false
)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_recap_hydration_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_recap_hydration_enable",
default = false
)
}

View File

@ -0,0 +1,45 @@
package com.twitter.timelineranker.parameters.recap_hydration
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.recap_hydration.RecapHydrationParams._
import com.twitter.timelineranker.parameters.util.ConfigHelper
import com.twitter.timelines.configapi._
object RecapHydrationProduction {
val deciderByParam: Map[Param[_], DeciderKeyName] = Map[Param[_], DeciderKeyName](
EnableContentFeaturesHydrationParam -> DeciderKey.RecapHydrationEnableContentFeaturesHydration
)
val booleanParams: Seq[EnableContentFeaturesHydrationParam.type] = Seq(
EnableContentFeaturesHydrationParam
)
val booleanFeatureSwitchParams: Seq[FSParam[Boolean]] = Seq(
EnableTokensInContentFeaturesHydrationParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableConversationControlInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam
)
}
class RecapHydrationProduction(deciderGateBuilder: DeciderGateBuilder) {
val configHelper: ConfigHelper =
new ConfigHelper(RecapHydrationProduction.deciderByParam, deciderGateBuilder)
val booleanOverrides: Seq[OptionalOverride[Boolean]] =
configHelper.createDeciderBasedBooleanOverrides(RecapHydrationProduction.booleanParams)
val booleanFeatureSwitchOverrides: Seq[OptionalOverride[Boolean]] =
FeatureSwitchOverrideUtil.getBooleanFSOverrides(
RecapHydrationProduction.booleanFeatureSwitchParams: _*
)
val config: BaseConfig = new BaseConfigBuilder()
.set(
booleanOverrides: _*
).set(
booleanFeatureSwitchOverrides: _*
)
.build(RecapHydrationProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,18 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider",
"timelineranker/common/src/main/scala/com/twitter/timelineranker/model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/config",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
"timelines/src/main/scala/com/twitter/timelines/config",
"timelines/src/main/scala/com/twitter/timelines/decider",
"timelines/src/main/scala/com/twitter/timelines/util/bounds",
"util/util-stats/src/main/scala",
],
)

View File

@ -0,0 +1,45 @@
package com.twitter.timelineranker.parameters.revchron
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
object ReverseChronParams {
import ReverseChronTimelineQueryContext._
/**
* Controls limit on the number of followed users fetched from SGS when materializing home timelines.
*/
object MaxFollowedUsersParam
extends FSBoundedParam(
"reverse_chron_max_followed_users",
default = MaxFollowedUsers.default,
min = MaxFollowedUsers.bounds.minInclusive,
max = MaxFollowedUsers.bounds.maxInclusive
)
object ReturnEmptyWhenOverMaxFollowsParam
extends FSParam(
name = "reverse_chron_return_empty_when_over_max_follows",
default = true
)
/**
* When true, search requests for the reverse chron timeline will include an additional operator
* so that search will not return tweets that are directed at non-followed users.
*/
object DirectedAtNarrowcastingViaSearchParam
extends FSParam(
name = "reverse_chron_directed_at_narrowcasting_via_search",
default = false
)
/**
* When true, search requests for the reverse chron timeline will request additional metadata
* from search and use this metadata for post filtering.
*/
object PostFilteringBasedOnSearchMetadataEnabledParam
extends FSParam(
name = "reverse_chron_post_filtering_based_on_search_metadata_enabled",
default = true
)
}

View File

@ -0,0 +1,28 @@
package com.twitter.timelineranker.parameters.revchron
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.timelines.configapi._
object ReverseChronProduction {
val intFeatureSwitchParams = Seq(ReverseChronParams.MaxFollowedUsersParam)
val booleanFeatureSwitchParams = Seq(
ReverseChronParams.ReturnEmptyWhenOverMaxFollowsParam,
ReverseChronParams.DirectedAtNarrowcastingViaSearchParam,
ReverseChronParams.PostFilteringBasedOnSearchMetadataEnabledParam
)
}
class ReverseChronProduction(deciderGateBuilder: DeciderGateBuilder) {
val intOverrides = FeatureSwitchOverrideUtil.getBoundedIntFSOverrides(
ReverseChronProduction.intFeatureSwitchParams: _*
)
val booleanOverrides = FeatureSwitchOverrideUtil.getBooleanFSOverrides(
ReverseChronProduction.booleanFeatureSwitchParams: _*
)
val config: BaseConfig = new BaseConfigBuilder()
.set(intOverrides: _*)
.set(booleanOverrides: _*)
.build(ReverseChronProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,114 @@
package com.twitter.timelineranker.parameters.revchron
import com.twitter.timelineranker.model.ReverseChronTimelineQuery
import com.twitter.timelines.util.bounds.BoundsWithDefault
import com.twitter.timelineservice.model.core.TimelineKind
import com.twitter.timelineservice.model.core.TimelineLimits
object ReverseChronTimelineQueryContext {
val MaxCountLimit: Int = TimelineLimits.default.lengthLimit(TimelineKind.home)
val MaxCount: BoundsWithDefault[Int] = BoundsWithDefault[Int](0, MaxCountLimit, MaxCountLimit)
val MaxCountMultiplier: BoundsWithDefault[Double] = BoundsWithDefault[Double](0.5, 2.0, 1.0)
val MaxFollowedUsers: BoundsWithDefault[Int] = BoundsWithDefault[Int](1, 15000, 5000)
val TweetsFilteringLossageThresholdPercent: BoundsWithDefault[Int] =
BoundsWithDefault[Int](10, 100, 20)
val TweetsFilteringLossageLimitPercent: BoundsWithDefault[Int] =
BoundsWithDefault[Int](40, 65, 60)
def getDefaultContext(query: ReverseChronTimelineQuery): ReverseChronTimelineQueryContext = {
new ReverseChronTimelineQueryContextImpl(
query,
getMaxCount = () => MaxCount.default,
getMaxCountMultiplier = () => MaxCountMultiplier.default,
getMaxFollowedUsers = () => MaxFollowedUsers.default,
getReturnEmptyWhenOverMaxFollows = () => true,
getDirectedAtNarrowastingViaSearch = () => false,
getPostFilteringBasedOnSearchMetadataEnabled = () => true,
getBackfillFilteredEntries = () => false,
getTweetsFilteringLossageThresholdPercent = () =>
TweetsFilteringLossageThresholdPercent.default,
getTweetsFilteringLossageLimitPercent = () => TweetsFilteringLossageLimitPercent.default
)
}
}
// Note that methods that return parameter value always use () to indicate that
// side effects may be involved in their invocation.
// for example, A likely side effect is to cause experiment impression.
trait ReverseChronTimelineQueryContext {
def query: ReverseChronTimelineQuery
// Maximum number of tweets to be returned to caller.
def maxCount(): Int
// Multiplier applied to the number of tweets fetched from search expressed as percentage.
// It can be used to fetch more than the number tweets requested by a caller (to improve similarity)
// or to fetch less than requested to reduce load.
def maxCountMultiplier(): Double
// Maximum number of followed user accounts to use when materializing home timelines.
def maxFollowedUsers(): Int
// When true, if the user follows more than maxFollowedUsers, return an empty timeline.
def returnEmptyWhenOverMaxFollows(): Boolean
// When true, appends an operator for directed-at narrowcasting to the home materialization
// search request
def directedAtNarrowcastingViaSearch(): Boolean
// When true, requests additional metadata from search and use this metadata for post filtering.
def postFilteringBasedOnSearchMetadataEnabled(): Boolean
// Controls whether to back-fill timeline entries that get filtered out by TweetsPostFilter
// during home timeline materialization.
def backfillFilteredEntries(): Boolean
// If back-filling filtered entries is enabled and if number of tweets that get filtered out
// exceed this percentage then we will issue a second call to get more tweets.
def tweetsFilteringLossageThresholdPercent(): Int
// We need to ensure that the number of tweets requested by the second call
// are not unbounded (for example, if everything is filtered out in the first call)
// therefore we adjust the actual filtered out percentage to be no greater than
// the value below.
def tweetsFilteringLossageLimitPercent(): Int
// We need to indicate to search if we should use the archive cluster
// this option will come from ReverseChronTimelineQueryOptions and
// will be `true` by default if the options are not present.
def getTweetsFromArchiveIndex(): Boolean =
query.options.map(_.getTweetsFromArchiveIndex).getOrElse(true)
}
class ReverseChronTimelineQueryContextImpl(
override val query: ReverseChronTimelineQuery,
getMaxCount: () => Int,
getMaxCountMultiplier: () => Double,
getMaxFollowedUsers: () => Int,
getReturnEmptyWhenOverMaxFollows: () => Boolean,
getDirectedAtNarrowastingViaSearch: () => Boolean,
getPostFilteringBasedOnSearchMetadataEnabled: () => Boolean,
getBackfillFilteredEntries: () => Boolean,
getTweetsFilteringLossageThresholdPercent: () => Int,
getTweetsFilteringLossageLimitPercent: () => Int)
extends ReverseChronTimelineQueryContext {
override def maxCount(): Int = { getMaxCount() }
override def maxCountMultiplier(): Double = { getMaxCountMultiplier() }
override def maxFollowedUsers(): Int = { getMaxFollowedUsers() }
override def backfillFilteredEntries(): Boolean = { getBackfillFilteredEntries() }
override def tweetsFilteringLossageThresholdPercent(): Int = {
getTweetsFilteringLossageThresholdPercent()
}
override def tweetsFilteringLossageLimitPercent(): Int = {
getTweetsFilteringLossageLimitPercent()
}
override def returnEmptyWhenOverMaxFollows(): Boolean = {
getReturnEmptyWhenOverMaxFollows()
}
override def directedAtNarrowcastingViaSearch(): Boolean = {
getDirectedAtNarrowastingViaSearch()
}
override def postFilteringBasedOnSearchMetadataEnabled(): Boolean = {
getPostFilteringBasedOnSearchMetadataEnabled()
}
}

View File

@ -0,0 +1,72 @@
package com.twitter.timelineranker.parameters.revchron
import com.twitter.timelineranker.config.RuntimeConfiguration
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.model._
import com.twitter.timelineranker.parameters.util.RequestContextBuilder
import com.twitter.timelines.configapi.Config
import com.twitter.timelines.decider.FeatureValue
import com.twitter.util.Future
object ReverseChronTimelineQueryContextBuilder {
val MaxCountLimitKey: Seq[String] = Seq("search_request_max_count_limit")
}
class ReverseChronTimelineQueryContextBuilder(
config: Config,
runtimeConfig: RuntimeConfiguration,
requestContextBuilder: RequestContextBuilder) {
import ReverseChronTimelineQueryContext._
import ReverseChronTimelineQueryContextBuilder._
private val maxCountMultiplier = FeatureValue(
runtimeConfig.deciderGateBuilder,
DeciderKey.MultiplierOfMaterializationTweetsFetched,
MaxCountMultiplier,
value => (value / 100.0)
)
private val backfillFilteredEntriesGate =
runtimeConfig.deciderGateBuilder.linearGate(DeciderKey.BackfillFilteredEntries)
private val tweetsFilteringLossageThresholdPercent = FeatureValue(
runtimeConfig.deciderGateBuilder,
DeciderKey.TweetsFilteringLossageThreshold,
TweetsFilteringLossageThresholdPercent,
value => (value / 100)
)
private val tweetsFilteringLossageLimitPercent = FeatureValue(
runtimeConfig.deciderGateBuilder,
DeciderKey.TweetsFilteringLossageLimit,
TweetsFilteringLossageLimitPercent,
value => (value / 100)
)
private def getMaxCountFromConfigStore(): Int = {
runtimeConfig.configStore.getAsInt(MaxCountLimitKey).getOrElse(MaxCount.default)
}
def apply(query: ReverseChronTimelineQuery): Future[ReverseChronTimelineQueryContext] = {
requestContextBuilder(Some(query.userId), deviceContext = None).map { baseContext =>
val params = config(baseContext, runtimeConfig.statsReceiver)
new ReverseChronTimelineQueryContextImpl(
query,
getMaxCount = () => getMaxCountFromConfigStore(),
getMaxCountMultiplier = () => maxCountMultiplier(),
getMaxFollowedUsers = () => params(ReverseChronParams.MaxFollowedUsersParam),
getReturnEmptyWhenOverMaxFollows =
() => params(ReverseChronParams.ReturnEmptyWhenOverMaxFollowsParam),
getDirectedAtNarrowastingViaSearch =
() => params(ReverseChronParams.DirectedAtNarrowcastingViaSearchParam),
getPostFilteringBasedOnSearchMetadataEnabled =
() => params(ReverseChronParams.PostFilteringBasedOnSearchMetadataEnabledParam),
getBackfillFilteredEntries = () => backfillFilteredEntriesGate(),
getTweetsFilteringLossageThresholdPercent = () => tweetsFilteringLossageThresholdPercent(),
getTweetsFilteringLossageLimitPercent = () => tweetsFilteringLossageLimitPercent()
)
}
}
}

View File

@ -0,0 +1,13 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"servo/decider/src/main/scala",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/decider",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/parameters/util",
"timelines/src/main/scala/com/twitter/timelines/util/bounds",
],
)

View File

@ -0,0 +1,174 @@
package com.twitter.timelineranker.parameters.uteg_liked_by_tweets
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.Param
import com.twitter.timelines.util.bounds.BoundsWithDefault
object UtegLikedByTweetsParams {
val ProbabilityRandomTweet: BoundsWithDefault[Double] = BoundsWithDefault[Double](0.0, 1.0, 0.0)
object DefaultUTEGInNetworkCount extends Param(200)
object DefaultUTEGOutOfNetworkCount extends Param(800)
object DefaultMaxTweetCount extends Param(200)
/**
* Enables semantic core, penguin, and tweetypie content features in uteg liked by tweets source.
*/
object EnableContentFeaturesHydrationParam extends Param(false)
/**
* additionally enables tokens when hydrating content features.
*/
object EnableTokensInContentFeaturesHydrationParam
extends FSParam(
name = "uteg_liked_by_tweets_enable_tokens_in_content_features_hydration",
default = false
)
/**
* additionally enables tweet text when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableTweetTextInContentFeaturesHydrationParam
extends FSParam(
name = "uteg_liked_by_tweets_enable_tweet_text_in_content_features_hydration",
default = false
)
/**
* A multiplier for earlybird score when combining earlybird score and real graph score for ranking.
* Note multiplier for realgraph score := 1.0, and only change earlybird score multiplier.
*/
object EarlybirdScoreMultiplierParam
extends FSBoundedParam(
"uteg_liked_by_tweets_earlybird_score_multiplier_param",
1.0,
0,
20.0
)
object UTEGRecommendationsFilter {
/**
* enable filtering of UTEG recommendations based on social proof type
*/
object EnableParam
extends FSParam(
"uteg_liked_by_tweets_uteg_recommendations_filter_enable",
false
)
/**
* filters out UTEG recommendations that have been tweeted by someone the user follows
*/
object ExcludeTweetParam
extends FSParam(
"uteg_liked_by_tweets_uteg_recommendations_filter_exclude_tweet",
false
)
/**
* filters out UTEG recommendations that have been retweeted by someone the user follows
*/
object ExcludeRetweetParam
extends FSParam(
"uteg_liked_by_tweets_uteg_recommendations_filter_exclude_retweet",
false
)
/**
* filters out UTEG recommendations that have been replied to by someone the user follows
* not filtering out the replies
*/
object ExcludeReplyParam
extends FSParam(
"uteg_liked_by_tweets_uteg_recommendations_filter_exclude_reply",
false
)
/**
* filters out UTEG recommendations that have been quoted by someone the user follows
*/
object ExcludeQuoteTweetParam
extends FSParam(
"uteg_liked_by_tweets_uteg_recommendations_filter_exclude_quote",
false
)
/**
* filters out recommended replies that have been directed at out of network users.
*/
object ExcludeRecommendedRepliesToNonFollowedUsersParam
extends FSParam(
name =
"uteg_liked_by_tweets_uteg_recommendations_filter_exclude_recommended_replies_to_non_followed_users",
default = false
)
}
/**
* Minimum number of favorited-by users required for recommended tweets.
*/
object MinNumFavoritedByUserIdsParam extends Param(1)
/**
* Includes one or multiple random tweets in the response.
*/
object IncludeRandomTweetParam
extends FSParam(name = "uteg_liked_by_tweets_include_random_tweet", default = false)
/**
* One single random tweet (true) or tag tweet as random with given probability (false).
*/
object IncludeSingleRandomTweetParam
extends FSParam(name = "uteg_liked_by_tweets_include_random_tweet_single", default = false)
/**
* Probability to tag a tweet as random (will not be ranked).
*/
object ProbabilityRandomTweetParam
extends FSBoundedParam(
name = "uteg_liked_by_tweets_include_random_tweet_probability",
default = ProbabilityRandomTweet.default,
min = ProbabilityRandomTweet.bounds.minInclusive,
max = ProbabilityRandomTweet.bounds.maxInclusive)
/**
* additionally enables conversationControl when hydrating content features.
* This only works if EnableContentFeaturesHydrationParam is set to true
*/
object EnableConversationControlInContentFeaturesHydrationParam
extends FSParam(
name = "conversation_control_in_content_features_hydration_uteg_liked_by_tweets_enable",
default = false
)
object EnableTweetMediaHydrationParam
extends FSParam(
name = "tweet_media_hydration_uteg_liked_by_tweets_enable",
default = false
)
object NumAdditionalRepliesParam
extends FSBoundedParam(
name = "uteg_liked_by_tweets_num_additional_replies",
default = 0,
min = 0,
max = 1000
)
/**
* Enable relevance search, otherwise recency search from earlybird.
*/
object EnableRelevanceSearchParam
extends FSParam(
name = "uteg_liked_by_tweets_enable_relevance_search",
default = true
)
}

View File

@ -0,0 +1,87 @@
package com.twitter.timelineranker.parameters.uteg_liked_by_tweets
import com.twitter.servo.decider.DeciderGateBuilder
import com.twitter.servo.decider.DeciderKeyName
import com.twitter.timelineranker.decider.DeciderKey
import com.twitter.timelineranker.parameters.uteg_liked_by_tweets.UtegLikedByTweetsParams._
import com.twitter.timelineranker.parameters.util.ConfigHelper
import com.twitter.timelines.configapi.BaseConfig
import com.twitter.timelines.configapi.BaseConfigBuilder
import com.twitter.timelines.configapi.FSBoundedParam
import com.twitter.timelines.configapi.FSParam
import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil
import com.twitter.timelines.configapi.OptionalOverride
import com.twitter.timelines.configapi.Param
object UtegLikedByTweetsProduction {
val deciderByParam: Map[Param[_], DeciderKeyName] = Map[Param[_], DeciderKeyName](
EnableContentFeaturesHydrationParam -> DeciderKey.UtegLikedByTweetsEnableContentFeaturesHydration
)
val booleanDeciderParams: Seq[EnableContentFeaturesHydrationParam.type] = Seq(
EnableContentFeaturesHydrationParam
)
val intParams: Seq[Param[Int]] = Seq(
DefaultUTEGInNetworkCount,
DefaultMaxTweetCount,
DefaultUTEGOutOfNetworkCount,
MinNumFavoritedByUserIdsParam
)
val booleanFeatureSwitchParams: Seq[FSParam[Boolean]] = Seq(
UTEGRecommendationsFilter.EnableParam,
UTEGRecommendationsFilter.ExcludeQuoteTweetParam,
UTEGRecommendationsFilter.ExcludeReplyParam,
UTEGRecommendationsFilter.ExcludeRetweetParam,
UTEGRecommendationsFilter.ExcludeTweetParam,
EnableTokensInContentFeaturesHydrationParam,
EnableConversationControlInContentFeaturesHydrationParam,
UTEGRecommendationsFilter.ExcludeRecommendedRepliesToNonFollowedUsersParam,
EnableTweetTextInContentFeaturesHydrationParam,
EnableTweetMediaHydrationParam,
UtegLikedByTweetsParams.IncludeRandomTweetParam,
UtegLikedByTweetsParams.IncludeSingleRandomTweetParam,
UtegLikedByTweetsParams.EnableRelevanceSearchParam
)
val boundedDoubleFeatureSwitchParams: Seq[FSBoundedParam[Double]] = Seq(
EarlybirdScoreMultiplierParam,
UtegLikedByTweetsParams.ProbabilityRandomTweetParam
)
val boundedIntFeatureSwitchParams: Seq[FSBoundedParam[Int]] = Seq(
UtegLikedByTweetsParams.NumAdditionalRepliesParam
)
}
class UtegLikedByTweetsProduction(deciderGateBuilder: DeciderGateBuilder) {
val configHelper: ConfigHelper =
new ConfigHelper(UtegLikedByTweetsProduction.deciderByParam, deciderGateBuilder)
val booleanDeciderOverrides: Seq[OptionalOverride[Boolean]] =
configHelper.createDeciderBasedBooleanOverrides(
UtegLikedByTweetsProduction.booleanDeciderParams)
val boundedDoubleFeatureSwitchOverrides: Seq[OptionalOverride[Double]] =
FeatureSwitchOverrideUtil.getBoundedDoubleFSOverrides(
UtegLikedByTweetsProduction.boundedDoubleFeatureSwitchParams: _*)
val booleanFeatureSwitchOverrides: Seq[OptionalOverride[Boolean]] =
FeatureSwitchOverrideUtil.getBooleanFSOverrides(
UtegLikedByTweetsProduction.booleanFeatureSwitchParams: _*)
val boundedIntFeaturesSwitchOverrides: Seq[OptionalOverride[Int]] =
FeatureSwitchOverrideUtil.getBoundedIntFSOverrides(
UtegLikedByTweetsProduction.boundedIntFeatureSwitchParams: _*)
val config: BaseConfig = new BaseConfigBuilder()
.set(
booleanDeciderOverrides: _*
)
.set(
boundedDoubleFeatureSwitchOverrides: _*
)
.set(
booleanFeatureSwitchOverrides: _*
)
.set(
boundedIntFeaturesSwitchOverrides: _*
)
.build(UtegLikedByTweetsProduction.getClass.getSimpleName)
}

View File

@ -0,0 +1,24 @@
scala_library(
sources = ["*.scala"],
compiler_option_sets = ["fatal_warnings"],
strict_deps = True,
tags = ["bazel-compatible"],
dependencies = [
"configapi/configapi-core",
"configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi",
"configapi/configapi-decider",
"decider/src/main/scala",
"servo/decider",
"servo/util/src/main/scala",
"timelineranker/common:model",
"timelineranker/server/src/main/scala/com/twitter/timelineranker/config",
"timelines/src/main/scala/com/twitter/timelines/common/model",
"timelines/src/main/scala/com/twitter/timelines/config",
"timelines/src/main/scala/com/twitter/timelines/config/configapi",
"timelines/src/main/scala/com/twitter/timelines/earlybird/common/utils",
"timelines/src/main/scala/com/twitter/timelines/model/candidate",
"timelines/src/main/scala/com/twitter/timelines/util",
"timelineservice/common/src/main/scala/com/twitter/timelineservice/model",
"util/util-stats/src/main/scala/com/twitter/finagle/stats",
],
)

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