mirror of
https://github.com/twitter/the-algorithm.git
synced 2025-01-11 19:59:10 +01:00
[docx] split commit for file 1400
Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
parent
c80f53f99d
commit
e27b2e31c3
Binary file not shown.
@ -1,31 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.server.modules
|
|
||||||
|
|
||||||
import com.twitter.inject.TwitterModule
|
|
||||||
|
|
||||||
object ServerFlagNames {
|
|
||||||
final val NumWorkers = "service.num_workers"
|
|
||||||
final val ServiceRole = "service.role"
|
|
||||||
final val ServiceEnv = "service.env"
|
|
||||||
|
|
||||||
final val MemCacheClientName = "service.mem_cache_client_name"
|
|
||||||
final val MemCachePath = "service.mem_cache_path"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes references to the flag values defined in the aurora.deploy file.
|
|
||||||
* To check what the flag values are initialized in runtime, search FlagsModule in stdout
|
|
||||||
*/
|
|
||||||
object ServerFlagsModule extends TwitterModule {
|
|
||||||
|
|
||||||
import ServerFlagNames._
|
|
||||||
|
|
||||||
flag[Int](NumWorkers, "Num of workers")
|
|
||||||
|
|
||||||
flag[String](ServiceRole, "Service Role")
|
|
||||||
|
|
||||||
flag[String](ServiceEnv, "Service Env")
|
|
||||||
|
|
||||||
flag[String](MemCacheClientName, "MemCache Client Name")
|
|
||||||
|
|
||||||
flag[String](MemCachePath, "MemCache Path")
|
|
||||||
}
|
|
Binary file not shown.
@ -1,16 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.server.stores
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.common.Configs.RandomSeed
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.FeatureType
|
|
||||||
import scala.util.hashing.MurmurHash3
|
|
||||||
|
|
||||||
object FeatureTypesEncoder {
|
|
||||||
|
|
||||||
def apply(featureTypes: Seq[FeatureType]): String = {
|
|
||||||
val byteArray = featureTypes.flatMap { featureType =>
|
|
||||||
Array(featureType.leftEdgeType.getValue.toByte, featureType.rightEdgeType.getValue.toByte)
|
|
||||||
}.toArray
|
|
||||||
(MurmurHash3.bytesHash(byteArray, RandomSeed) & 0x7fffffff).toString // keep positive
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Binary file not shown.
@ -1,181 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.server.stores
|
|
||||||
|
|
||||||
import com.twitter.finagle.RequestTimeoutException
|
|
||||||
import com.twitter.finagle.stats.{Stat, StatsReceiver}
|
|
||||||
import com.twitter.graph_feature_service.server.handlers.ServerGetIntersectionHandler.GetIntersectionRequest
|
|
||||||
import com.twitter.graph_feature_service.server.modules.GraphFeatureServiceWorkerClients
|
|
||||||
import com.twitter.graph_feature_service.server.stores.GetIntersectionStore.GetIntersectionQuery
|
|
||||||
import com.twitter.graph_feature_service.thriftscala._
|
|
||||||
import com.twitter.inject.Logging
|
|
||||||
import com.twitter.storehaus.ReadableStore
|
|
||||||
import com.twitter.util.Future
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
case class GetIntersectionStore(
|
|
||||||
graphFeatureServiceWorkerClients: GraphFeatureServiceWorkerClients,
|
|
||||||
statsReceiver: StatsReceiver)
|
|
||||||
extends ReadableStore[GetIntersectionQuery, CachedIntersectionResult]
|
|
||||||
with Logging {
|
|
||||||
|
|
||||||
import GetIntersectionStore._
|
|
||||||
|
|
||||||
private val stats = statsReceiver.scope("get_intersection_store")
|
|
||||||
private val requestCount = stats.counter(name = "request_count")
|
|
||||||
private val aggregatorLatency = stats.stat("aggregator_latency")
|
|
||||||
private val timeOutCounter = stats.counter("worker_timeouts")
|
|
||||||
private val unknownErrorCounter = stats.counter("unknown_errors")
|
|
||||||
|
|
||||||
override def multiGet[K1 <: GetIntersectionQuery](
|
|
||||||
ks: Set[K1]
|
|
||||||
): Map[K1, Future[Option[CachedIntersectionResult]]] = {
|
|
||||||
if (ks.isEmpty) {
|
|
||||||
Map.empty
|
|
||||||
} else {
|
|
||||||
requestCount.incr()
|
|
||||||
|
|
||||||
val head = ks.head
|
|
||||||
// We assume all the GetIntersectionQuery use the same userId and featureTypes
|
|
||||||
val userId = head.userId
|
|
||||||
val featureTypes = head.featureTypes
|
|
||||||
val presetFeatureTypes = head.presetFeatureTypes
|
|
||||||
val calculatedFeatureTypes = head.calculatedFeatureTypes
|
|
||||||
val intersectionIdLimit = head.intersectionIdLimit
|
|
||||||
|
|
||||||
val request = WorkerIntersectionRequest(
|
|
||||||
userId,
|
|
||||||
ks.map(_.candidateId).toArray,
|
|
||||||
featureTypes,
|
|
||||||
presetFeatureTypes,
|
|
||||||
intersectionIdLimit
|
|
||||||
)
|
|
||||||
|
|
||||||
val resultFuture = Future
|
|
||||||
.collect(
|
|
||||||
graphFeatureServiceWorkerClients.workers.map { worker =>
|
|
||||||
worker
|
|
||||||
.getIntersection(request)
|
|
||||||
.rescue {
|
|
||||||
case _: RequestTimeoutException =>
|
|
||||||
timeOutCounter.incr()
|
|
||||||
Future.value(DefaultWorkerIntersectionResponse)
|
|
||||||
case e =>
|
|
||||||
unknownErrorCounter.incr()
|
|
||||||
logger.error("Failure to load result.", e)
|
|
||||||
Future.value(DefaultWorkerIntersectionResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).map { responses =>
|
|
||||||
Stat.time(aggregatorLatency) {
|
|
||||||
gfsIntersectionResponseAggregator(
|
|
||||||
responses,
|
|
||||||
calculatedFeatureTypes,
|
|
||||||
request.candidateUserIds,
|
|
||||||
intersectionIdLimit
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ks.map { query =>
|
|
||||||
query -> resultFuture.map(_.get(query.candidateId))
|
|
||||||
}.toMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to merge GfsIntersectionResponse from workers into one result.
|
|
||||||
*/
|
|
||||||
private def gfsIntersectionResponseAggregator(
|
|
||||||
responseList: Seq[WorkerIntersectionResponse],
|
|
||||||
features: Seq[FeatureType],
|
|
||||||
candidates: Seq[Long],
|
|
||||||
intersectionIdLimit: Int
|
|
||||||
): Map[Long, CachedIntersectionResult] = {
|
|
||||||
|
|
||||||
// Map of (candidate -> features -> type -> value)
|
|
||||||
val cube = Array.fill[Int](candidates.length, features.length, 3)(0)
|
|
||||||
// Map of (candidate -> features -> intersectionIds)
|
|
||||||
val ids = Array.fill[Option[ArrayBuffer[Long]]](candidates.length, features.length)(None)
|
|
||||||
val notZero = intersectionIdLimit != 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
response <- responseList
|
|
||||||
(features, candidateIndex) <- response.results.zipWithIndex
|
|
||||||
(workerValue, featureIndex) <- features.zipWithIndex
|
|
||||||
} {
|
|
||||||
cube(candidateIndex)(featureIndex)(CountIndex) += workerValue.count
|
|
||||||
cube(candidateIndex)(featureIndex)(LeftDegreeIndex) += workerValue.leftNodeDegree
|
|
||||||
cube(candidateIndex)(featureIndex)(RightDegreeIndex) += workerValue.rightNodeDegree
|
|
||||||
|
|
||||||
if (notZero && workerValue.intersectionIds.nonEmpty) {
|
|
||||||
val arrayBuffer = ids(candidateIndex)(featureIndex) match {
|
|
||||||
case Some(buffer) => buffer
|
|
||||||
case None =>
|
|
||||||
val buffer = ArrayBuffer[Long]()
|
|
||||||
ids(candidateIndex)(featureIndex) = Some(buffer)
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
val intersectionIds = workerValue.intersectionIds
|
|
||||||
|
|
||||||
// Scan the intersectionId based on the Shard. The response order is consistent.
|
|
||||||
if (arrayBuffer.size < intersectionIdLimit) {
|
|
||||||
if (intersectionIds.size > intersectionIdLimit - arrayBuffer.size) {
|
|
||||||
arrayBuffer ++= intersectionIds.slice(0, intersectionIdLimit - arrayBuffer.size)
|
|
||||||
} else {
|
|
||||||
arrayBuffer ++= intersectionIds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.zipWithIndex.map {
|
|
||||||
case (candidate, candidateIndex) =>
|
|
||||||
candidate -> CachedIntersectionResult(features.indices.map { featureIndex =>
|
|
||||||
WorkerIntersectionValue(
|
|
||||||
cube(candidateIndex)(featureIndex)(CountIndex),
|
|
||||||
cube(candidateIndex)(featureIndex)(LeftDegreeIndex),
|
|
||||||
cube(candidateIndex)(featureIndex)(RightDegreeIndex),
|
|
||||||
ids(candidateIndex)(featureIndex).getOrElse(Nil)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}.toMap
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object GetIntersectionStore {
|
|
||||||
|
|
||||||
private[graph_feature_service] case class GetIntersectionQuery(
|
|
||||||
userId: Long,
|
|
||||||
candidateId: Long,
|
|
||||||
featureTypes: Seq[FeatureType],
|
|
||||||
presetFeatureTypes: PresetFeatureTypes,
|
|
||||||
featureTypesString: String,
|
|
||||||
calculatedFeatureTypes: Seq[FeatureType],
|
|
||||||
intersectionIdLimit: Int)
|
|
||||||
|
|
||||||
private[graph_feature_service] object GetIntersectionQuery {
|
|
||||||
def buildQueries(request: GetIntersectionRequest): Set[GetIntersectionQuery] = {
|
|
||||||
request.candidateUserIds.toSet.map { candidateId: Long =>
|
|
||||||
GetIntersectionQuery(
|
|
||||||
request.userId,
|
|
||||||
candidateId,
|
|
||||||
request.featureTypes,
|
|
||||||
request.presetFeatureTypes,
|
|
||||||
request.calculatedFeatureTypesString,
|
|
||||||
request.calculatedFeatureTypes,
|
|
||||||
request.intersectionIdLimit.getOrElse(DefaultIntersectionIdLimit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't return the intersectionId for better performance
|
|
||||||
private val DefaultIntersectionIdLimit = 0
|
|
||||||
private val DefaultWorkerIntersectionResponse = WorkerIntersectionResponse()
|
|
||||||
|
|
||||||
private val CountIndex = 0
|
|
||||||
private val LeftDegreeIndex = 1
|
|
||||||
private val RightDegreeIndex = 2
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
platform = "java8",
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"graph-feature-service/src/main/thrift/com/twitter/graph_feature_service:graph_feature_service_thrift-scala",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,58 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.util
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.EdgeType._
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.{FeatureType, PresetFeatureTypes}
|
|
||||||
|
|
||||||
object FeatureTypesCalculator {
|
|
||||||
|
|
||||||
final val DefaultTwoHop = Seq(
|
|
||||||
FeatureType(Following, FollowedBy),
|
|
||||||
FeatureType(Following, FavoritedBy),
|
|
||||||
FeatureType(Following, RetweetedBy),
|
|
||||||
FeatureType(Following, MentionedBy),
|
|
||||||
FeatureType(Following, MutualFollow),
|
|
||||||
FeatureType(Favorite, FollowedBy),
|
|
||||||
FeatureType(Favorite, FavoritedBy),
|
|
||||||
FeatureType(Favorite, RetweetedBy),
|
|
||||||
FeatureType(Favorite, MentionedBy),
|
|
||||||
FeatureType(Favorite, MutualFollow),
|
|
||||||
FeatureType(MutualFollow, FollowedBy),
|
|
||||||
FeatureType(MutualFollow, FavoritedBy),
|
|
||||||
FeatureType(MutualFollow, RetweetedBy),
|
|
||||||
FeatureType(MutualFollow, MentionedBy),
|
|
||||||
FeatureType(MutualFollow, MutualFollow)
|
|
||||||
)
|
|
||||||
|
|
||||||
final val SocialProofTwoHop = Seq(FeatureType(Following, FollowedBy))
|
|
||||||
|
|
||||||
final val HtlTwoHop = DefaultTwoHop
|
|
||||||
|
|
||||||
final val WtfTwoHop = SocialProofTwoHop
|
|
||||||
|
|
||||||
final val SqTwoHop = DefaultTwoHop
|
|
||||||
|
|
||||||
final val RuxTwoHop = DefaultTwoHop
|
|
||||||
|
|
||||||
final val MRTwoHop = DefaultTwoHop
|
|
||||||
|
|
||||||
final val UserTypeaheadTwoHop = SocialProofTwoHop
|
|
||||||
|
|
||||||
final val presetFeatureTypes =
|
|
||||||
(HtlTwoHop ++ WtfTwoHop ++ SqTwoHop ++ RuxTwoHop ++ MRTwoHop ++ UserTypeaheadTwoHop).toSet
|
|
||||||
|
|
||||||
def getFeatureTypes(
|
|
||||||
presetFeatureTypes: PresetFeatureTypes,
|
|
||||||
featureTypes: Seq[FeatureType]
|
|
||||||
): Seq[FeatureType] = {
|
|
||||||
presetFeatureTypes match {
|
|
||||||
case PresetFeatureTypes.HtlTwoHop => HtlTwoHop
|
|
||||||
case PresetFeatureTypes.WtfTwoHop => WtfTwoHop
|
|
||||||
case PresetFeatureTypes.SqTwoHop => SqTwoHop
|
|
||||||
case PresetFeatureTypes.RuxTwoHop => RuxTwoHop
|
|
||||||
case PresetFeatureTypes.MrTwoHop => MRTwoHop
|
|
||||||
case PresetFeatureTypes.UserTypeaheadTwoHop => UserTypeaheadTwoHop
|
|
||||||
case _ => featureTypes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Binary file not shown.
@ -1,242 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.util
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.{
|
|
||||||
FeatureType,
|
|
||||||
IntersectionValue,
|
|
||||||
WorkerIntersectionValue
|
|
||||||
}
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions for computing feature values based on the values returned by constantDB.
|
|
||||||
*/
|
|
||||||
object IntersectionValueCalculator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the size of the array in a ByteBuffer.
|
|
||||||
* Note that this function assumes the ByteBuffer is encoded using Injections.seqLong2ByteBuffer
|
|
||||||
*/
|
|
||||||
def computeArraySize(x: ByteBuffer): Int = {
|
|
||||||
x.remaining() >> 3 // divide 8
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
def apply(x: ByteBuffer, y: ByteBuffer, intersectionIdLimit: Int): WorkerIntersectionValue = {
|
|
||||||
|
|
||||||
val xSize = computeArraySize(x)
|
|
||||||
val ySize = computeArraySize(y)
|
|
||||||
|
|
||||||
val largerArray = if (xSize > ySize) x else y
|
|
||||||
val smallerArray = if (xSize > ySize) y else x
|
|
||||||
|
|
||||||
if (intersectionIdLimit == 0) {
|
|
||||||
val result = computeIntersectionUsingBinarySearchOnLargerByteBuffer(smallerArray, largerArray)
|
|
||||||
WorkerIntersectionValue(result, xSize, ySize)
|
|
||||||
} else {
|
|
||||||
val (result, ids) = computeIntersectionWithIds(smallerArray, largerArray, intersectionIdLimit)
|
|
||||||
WorkerIntersectionValue(result, xSize, ySize, ids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note that this function assumes the ByteBuffer is encoded using Injections.seqLong2ByteBuffer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
def computeIntersectionUsingBinarySearchOnLargerByteBuffer(
|
|
||||||
smallArray: ByteBuffer,
|
|
||||||
largeArray: ByteBuffer
|
|
||||||
): Int = {
|
|
||||||
var res: Int = 0
|
|
||||||
var i: Int = 0
|
|
||||||
|
|
||||||
while (i < smallArray.remaining()) {
|
|
||||||
if (binarySearch(largeArray, smallArray.getLong(i)) >= 0) {
|
|
||||||
res += 1
|
|
||||||
}
|
|
||||||
i += 8
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
def computeIntersectionWithIds(
|
|
||||||
smallArray: ByteBuffer,
|
|
||||||
largeArray: ByteBuffer,
|
|
||||||
intersectionLimit: Int
|
|
||||||
): (Int, Seq[Long]) = {
|
|
||||||
var res: Int = 0
|
|
||||||
var i: Int = 0
|
|
||||||
// Most of the intersectionLimit is smaller than default size: 16
|
|
||||||
val idBuffer = ArrayBuffer[Long]()
|
|
||||||
|
|
||||||
while (i < smallArray.remaining()) {
|
|
||||||
val value = smallArray.getLong(i)
|
|
||||||
if (binarySearch(largeArray, value) >= 0) {
|
|
||||||
res += 1
|
|
||||||
// Always get the smaller ids
|
|
||||||
if (idBuffer.size < intersectionLimit) {
|
|
||||||
idBuffer += value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 8
|
|
||||||
}
|
|
||||||
(res, idBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note that this function assumes the ByteBuffer is encoded using Injections.seqLong2ByteBuffer
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private[util] def binarySearch(arr: ByteBuffer, value: Long): Int = {
|
|
||||||
var start = 0
|
|
||||||
var end = arr.remaining()
|
|
||||||
|
|
||||||
while (start <= end && start < arr.remaining()) {
|
|
||||||
val mid = ((start + end) >> 1) & ~7 // take mid - mid % 8
|
|
||||||
if (arr.getLong(mid) == value) {
|
|
||||||
return mid // return the index of the value
|
|
||||||
} else if (arr.getLong(mid) < value) {
|
|
||||||
start = mid + 8
|
|
||||||
} else {
|
|
||||||
end = mid - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if not existed, return -1
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: for now it only computes intersection size. Will add more feature types (e.g., dot
|
|
||||||
* product, maximum value).
|
|
||||||
*
|
|
||||||
* NOTE that this function assumes both x and y are SORTED arrays.
|
|
||||||
* In graph feature service, the sorting is done in the offline Scalding job.
|
|
||||||
*
|
|
||||||
* @param x source user's array
|
|
||||||
* @param y candidate user's array
|
|
||||||
* @param featureType feature type
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
def apply(x: Array[Long], y: Array[Long], featureType: FeatureType): IntersectionValue = {
|
|
||||||
|
|
||||||
val xSize = x.length
|
|
||||||
val ySize = y.length
|
|
||||||
|
|
||||||
val intersection =
|
|
||||||
if (xSize.min(ySize) * math.log(xSize.max(ySize)) < (xSize + ySize).toDouble) {
|
|
||||||
if (xSize < ySize) {
|
|
||||||
computeIntersectionUsingBinarySearchOnLargerArray(x, y)
|
|
||||||
} else {
|
|
||||||
computeIntersectionUsingBinarySearchOnLargerArray(y, x)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
computeIntersectionUsingListMerging(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
IntersectionValue(
|
|
||||||
featureType,
|
|
||||||
Some(intersection.toInt),
|
|
||||||
None, // return None for now
|
|
||||||
Some(xSize),
|
|
||||||
Some(ySize)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for computing the intersections of two SORTED arrays by list merging.
|
|
||||||
*
|
|
||||||
* @param x one array
|
|
||||||
* @param y another array
|
|
||||||
* @param ordering ordering function for comparing values of T
|
|
||||||
* @tparam T type
|
|
||||||
* @return The intersection size and the list of intersected elements
|
|
||||||
*/
|
|
||||||
private[util] def computeIntersectionUsingListMerging[T](
|
|
||||||
x: Array[T],
|
|
||||||
y: Array[T]
|
|
||||||
)(
|
|
||||||
implicit ordering: Ordering[T]
|
|
||||||
): Int = {
|
|
||||||
|
|
||||||
var res: Int = 0
|
|
||||||
var i: Int = 0
|
|
||||||
var j: Int = 0
|
|
||||||
|
|
||||||
while (i < x.length && j < y.length) {
|
|
||||||
val comp = ordering.compare(x(i), y(j))
|
|
||||||
if (comp > 0) j += 1
|
|
||||||
else if (comp < 0) i += 1
|
|
||||||
else {
|
|
||||||
res += 1
|
|
||||||
i += 1
|
|
||||||
j += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for computing the intersections of two arrays by binary search on the larger array.
|
|
||||||
* Note that the larger array MUST be SORTED.
|
|
||||||
*
|
|
||||||
* @param smallArray smaller array
|
|
||||||
* @param largeArray larger array
|
|
||||||
* @param ordering ordering function for comparing values of T
|
|
||||||
* @tparam T type
|
|
||||||
*
|
|
||||||
* @return The intersection size and the list of intersected elements
|
|
||||||
*/
|
|
||||||
private[util] def computeIntersectionUsingBinarySearchOnLargerArray[T](
|
|
||||||
smallArray: Array[T],
|
|
||||||
largeArray: Array[T]
|
|
||||||
)(
|
|
||||||
implicit ordering: Ordering[T]
|
|
||||||
): Int = {
|
|
||||||
var res: Int = 0
|
|
||||||
var i: Int = 0
|
|
||||||
while (i < smallArray.length) {
|
|
||||||
val currentValue: T = smallArray(i)
|
|
||||||
if (binarySearch(largeArray, currentValue) >= 0) {
|
|
||||||
res += 1
|
|
||||||
}
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function for doing the binary search
|
|
||||||
*
|
|
||||||
* @param arr array
|
|
||||||
* @param value the target value for searching
|
|
||||||
* @param ordering ordering function
|
|
||||||
* @tparam T type
|
|
||||||
* @return the index of element in the larger array.
|
|
||||||
* If there is no such element in the array, return -1.
|
|
||||||
*/
|
|
||||||
private[util] def binarySearch[T](
|
|
||||||
arr: Array[T],
|
|
||||||
value: T
|
|
||||||
)(
|
|
||||||
implicit ordering: Ordering[T]
|
|
||||||
): Int = {
|
|
||||||
var start = 0
|
|
||||||
var end = arr.length - 1
|
|
||||||
|
|
||||||
while (start <= end) {
|
|
||||||
val mid = (start + end) >> 1
|
|
||||||
val comp = ordering.compare(arr(mid), value)
|
|
||||||
if (comp == 0) {
|
|
||||||
return mid // return the index of the value
|
|
||||||
} else if (comp < 0) {
|
|
||||||
start = mid + 1
|
|
||||||
} else {
|
|
||||||
end = mid - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if not existed, return -1
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["**/*.scala"],
|
|
||||||
platform = "java8",
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/com/google/inject:guice",
|
|
||||||
"3rdparty/jvm/javax/inject:javax.inject",
|
|
||||||
"3rdparty/jvm/net/codingwell:scala-guice",
|
|
||||||
"discovery-common/src/main/scala/com/twitter/discovery/common/stats",
|
|
||||||
"finatra-internal/decider/src/main/scala",
|
|
||||||
"finatra-internal/gizmoduck/src/main/scala",
|
|
||||||
"finatra-internal/mtls-thriftmux/src/main/scala",
|
|
||||||
"finatra/inject/inject-app/src/main/scala",
|
|
||||||
"finatra/inject/inject-core/src/main/scala",
|
|
||||||
"finatra/inject/inject-server/src/main/scala",
|
|
||||||
"finatra/inject/inject-thrift-client/src/main/scala",
|
|
||||||
"finatra/inject/inject-utils/src/main/scala",
|
|
||||||
"frigate/frigate-common:constdb_util",
|
|
||||||
"graph-feature-service/src/main/resources",
|
|
||||||
"graph-feature-service/src/main/scala/com/twitter/graph_feature_service/common",
|
|
||||||
"graph-feature-service/src/main/scala/com/twitter/graph_feature_service/util",
|
|
||||||
"graph-feature-service/src/main/thrift/com/twitter/graph_feature_service:graph_feature_service_thrift-scala",
|
|
||||||
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/common",
|
|
||||||
"servo/request/src/main/scala",
|
|
||||||
"twitter-server-internal/src/main/scala",
|
|
||||||
"twitter-server/server/src/main/scala",
|
|
||||||
"util/util-app/src/main/scala",
|
|
||||||
"util/util-slf4j-api/src/main/scala",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,58 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker
|
|
||||||
|
|
||||||
import com.google.inject.Module
|
|
||||||
import com.twitter.finatra.decider.modules.DeciderModule
|
|
||||||
import com.twitter.finatra.gizmoduck.modules.TimerModule
|
|
||||||
import com.twitter.finatra.mtls.thriftmux.Mtls
|
|
||||||
import com.twitter.finatra.thrift.ThriftServer
|
|
||||||
import com.twitter.finatra.thrift.filters.{
|
|
||||||
LoggingMDCFilter,
|
|
||||||
StatsFilter,
|
|
||||||
ThriftMDCFilter,
|
|
||||||
TraceIdMDCFilter
|
|
||||||
}
|
|
||||||
import com.twitter.finatra.mtls.thriftmux.modules.MtlsThriftWebFormsModule
|
|
||||||
import com.twitter.finatra.thrift.routing.ThriftRouter
|
|
||||||
import com.twitter.graph_feature_service.thriftscala
|
|
||||||
import com.twitter.graph_feature_service.worker.controllers.WorkerController
|
|
||||||
import com.twitter.graph_feature_service.worker.handlers.WorkerWarmupHandler
|
|
||||||
import com.twitter.graph_feature_service.worker.modules.{
|
|
||||||
GraphContainerProviderModule,
|
|
||||||
WorkerFlagModule
|
|
||||||
}
|
|
||||||
import com.twitter.graph_feature_service.worker.util.GraphContainer
|
|
||||||
import com.twitter.inject.thrift.modules.ThriftClientIdModule
|
|
||||||
import com.twitter.util.Await
|
|
||||||
|
|
||||||
object Main extends WorkerMain
|
|
||||||
|
|
||||||
class WorkerMain extends ThriftServer with Mtls {
|
|
||||||
|
|
||||||
override val name = "graph_feature_service-worker"
|
|
||||||
|
|
||||||
override val modules: Seq[Module] = {
|
|
||||||
Seq(
|
|
||||||
WorkerFlagModule,
|
|
||||||
DeciderModule,
|
|
||||||
TimerModule,
|
|
||||||
ThriftClientIdModule,
|
|
||||||
GraphContainerProviderModule,
|
|
||||||
new MtlsThriftWebFormsModule[thriftscala.Worker.MethodPerEndpoint](this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def configureThrift(router: ThriftRouter): Unit = {
|
|
||||||
router
|
|
||||||
.filter[LoggingMDCFilter]
|
|
||||||
.filter[TraceIdMDCFilter]
|
|
||||||
.filter[ThriftMDCFilter]
|
|
||||||
.filter[StatsFilter]
|
|
||||||
.add[WorkerController]
|
|
||||||
}
|
|
||||||
|
|
||||||
override protected def warmup(): Unit = {
|
|
||||||
val graphContainer = injector.instance[GraphContainer]
|
|
||||||
Await.result(graphContainer.warmup)
|
|
||||||
handle[WorkerWarmupHandler]()
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,38 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.controllers
|
|
||||||
|
|
||||||
import com.twitter.discovery.common.stats.DiscoveryStatsFilter
|
|
||||||
import com.twitter.finagle.Service
|
|
||||||
import com.twitter.finagle.stats.StatsReceiver
|
|
||||||
import com.twitter.finatra.thrift.Controller
|
|
||||||
import com.twitter.graph_feature_service.thriftscala
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.Worker.GetIntersection
|
|
||||||
import com.twitter.graph_feature_service.thriftscala._
|
|
||||||
import com.twitter.graph_feature_service.worker.handlers._
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class WorkerController @Inject() (
|
|
||||||
workerGetIntersectionHandler: WorkerGetIntersectionHandler
|
|
||||||
)(
|
|
||||||
implicit statsReceiver: StatsReceiver)
|
|
||||||
extends Controller(thriftscala.Worker) {
|
|
||||||
|
|
||||||
// use DiscoveryStatsFilter to filter out exceptions out of our control
|
|
||||||
private val getIntersectionService: Service[
|
|
||||||
WorkerIntersectionRequest,
|
|
||||||
WorkerIntersectionResponse
|
|
||||||
] =
|
|
||||||
new DiscoveryStatsFilter[WorkerIntersectionRequest, WorkerIntersectionResponse](
|
|
||||||
statsReceiver.scope("srv").scope("get_intersection")
|
|
||||||
).andThen(Service.mk(workerGetIntersectionHandler))
|
|
||||||
|
|
||||||
val getIntersection: Service[GetIntersection.Args, WorkerIntersectionResponse] = { args =>
|
|
||||||
getIntersectionService(args.request).onFailure { throwable =>
|
|
||||||
logger.error(s"Failure to get intersection for request $args.", throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(GetIntersection) { getIntersection }
|
|
||||||
|
|
||||||
}
|
|
Binary file not shown.
@ -1,105 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.handlers
|
|
||||||
|
|
||||||
import com.twitter.finagle.stats.{Stat, StatsReceiver}
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.{
|
|
||||||
WorkerIntersectionRequest,
|
|
||||||
WorkerIntersectionResponse,
|
|
||||||
WorkerIntersectionValue
|
|
||||||
}
|
|
||||||
import com.twitter.graph_feature_service.util.{FeatureTypesCalculator, IntersectionValueCalculator}
|
|
||||||
import com.twitter.graph_feature_service.util.IntersectionValueCalculator._
|
|
||||||
import com.twitter.graph_feature_service.worker.util.GraphContainer
|
|
||||||
import com.twitter.servo.request.RequestHandler
|
|
||||||
import com.twitter.util.Future
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import javax.inject.{Inject, Singleton}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class WorkerGetIntersectionHandler @Inject() (
|
|
||||||
graphContainer: GraphContainer,
|
|
||||||
statsReceiver: StatsReceiver)
|
|
||||||
extends RequestHandler[WorkerIntersectionRequest, WorkerIntersectionResponse] {
|
|
||||||
|
|
||||||
import WorkerGetIntersectionHandler._
|
|
||||||
|
|
||||||
private val stats: StatsReceiver = statsReceiver.scope("srv/get_intersection")
|
|
||||||
private val numCandidatesCount = stats.counter("total_num_candidates")
|
|
||||||
private val toPartialGraphQueryStat = stats.stat("to_partial_graph_query_latency")
|
|
||||||
private val fromPartialGraphQueryStat = stats.stat("from_partial_graph_query_latency")
|
|
||||||
private val intersectionCalculationStat = stats.stat("computation_latency")
|
|
||||||
|
|
||||||
override def apply(request: WorkerIntersectionRequest): Future[WorkerIntersectionResponse] = {
|
|
||||||
|
|
||||||
numCandidatesCount.incr(request.candidateUserIds.length)
|
|
||||||
|
|
||||||
val userId = request.userId
|
|
||||||
|
|
||||||
// NOTE: do not change the order of candidates
|
|
||||||
val candidateIds = request.candidateUserIds
|
|
||||||
|
|
||||||
// NOTE: do not change the order of features
|
|
||||||
val featureTypes =
|
|
||||||
FeatureTypesCalculator.getFeatureTypes(request.presetFeatureTypes, request.featureTypes)
|
|
||||||
|
|
||||||
val leftEdges = featureTypes.map(_.leftEdgeType).distinct
|
|
||||||
val rightEdges = featureTypes.map(_.rightEdgeType).distinct
|
|
||||||
|
|
||||||
val rightEdgeMap = Stat.time(toPartialGraphQueryStat) {
|
|
||||||
rightEdges.map { rightEdge =>
|
|
||||||
val map = graphContainer.toPartialMap.get(rightEdge) match {
|
|
||||||
case Some(graph) =>
|
|
||||||
candidateIds.flatMap { candidateId =>
|
|
||||||
graph.apply(candidateId).map(candidateId -> _)
|
|
||||||
}.toMap
|
|
||||||
case None =>
|
|
||||||
Map.empty[Long, ByteBuffer]
|
|
||||||
}
|
|
||||||
rightEdge -> map
|
|
||||||
}.toMap
|
|
||||||
}
|
|
||||||
|
|
||||||
val leftEdgeMap = Stat.time(fromPartialGraphQueryStat) {
|
|
||||||
leftEdges.flatMap { leftEdge =>
|
|
||||||
graphContainer.toPartialMap.get(leftEdge).flatMap(_.apply(userId)).map(leftEdge -> _)
|
|
||||||
}.toMap
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = Stat.time(intersectionCalculationStat) {
|
|
||||||
WorkerIntersectionResponse(
|
|
||||||
// NOTE that candidate ordering is important
|
|
||||||
candidateIds.map { candidateId =>
|
|
||||||
// NOTE that the featureTypes ordering is important
|
|
||||||
featureTypes.map {
|
|
||||||
featureType =>
|
|
||||||
val leftNeighborsOpt = leftEdgeMap.get(featureType.leftEdgeType)
|
|
||||||
val rightNeighborsOpt =
|
|
||||||
rightEdgeMap.get(featureType.rightEdgeType).flatMap(_.get(candidateId))
|
|
||||||
|
|
||||||
if (leftNeighborsOpt.isEmpty && rightNeighborsOpt.isEmpty) {
|
|
||||||
EmptyWorkerIntersectionValue
|
|
||||||
} else if (rightNeighborsOpt.isEmpty) {
|
|
||||||
EmptyWorkerIntersectionValue.copy(
|
|
||||||
leftNodeDegree = computeArraySize(leftNeighborsOpt.get)
|
|
||||||
)
|
|
||||||
} else if (leftNeighborsOpt.isEmpty) {
|
|
||||||
EmptyWorkerIntersectionValue.copy(
|
|
||||||
rightNodeDegree = computeArraySize(rightNeighborsOpt.get)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
IntersectionValueCalculator(
|
|
||||||
leftNeighborsOpt.get,
|
|
||||||
rightNeighborsOpt.get,
|
|
||||||
request.intersectionIdLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Future.value(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object WorkerGetIntersectionHandler {
|
|
||||||
val EmptyWorkerIntersectionValue: WorkerIntersectionValue = WorkerIntersectionValue(0, 0, 0, Nil)
|
|
||||||
}
|
|
Binary file not shown.
@ -1,14 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.handlers
|
|
||||||
|
|
||||||
import com.twitter.finatra.thrift.routing.ThriftWarmup
|
|
||||||
import com.twitter.inject.Logging
|
|
||||||
import com.twitter.inject.utils.Handler
|
|
||||||
import javax.inject.{Inject, Singleton}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class WorkerWarmupHandler @Inject() (warmup: ThriftWarmup) extends Handler with Logging {
|
|
||||||
|
|
||||||
override def handle(): Unit = {
|
|
||||||
info("Warmup Done!")
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,62 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.modules
|
|
||||||
|
|
||||||
import com.google.inject.Provides
|
|
||||||
import com.twitter.concurrent.AsyncSemaphore
|
|
||||||
import com.twitter.finagle.stats.StatsReceiver
|
|
||||||
import com.twitter.graph_feature_service.common.Configs._
|
|
||||||
import com.twitter.graph_feature_service.worker.util
|
|
||||||
import com.twitter.graph_feature_service.worker.util.AutoUpdatingGraph
|
|
||||||
import com.twitter.graph_feature_service.worker.util.FollowedByPartialValueGraph
|
|
||||||
import com.twitter.graph_feature_service.worker.util.FollowingPartialValueGraph
|
|
||||||
import com.twitter.graph_feature_service.worker.util.GraphContainer
|
|
||||||
import com.twitter.graph_feature_service.worker.util.GraphKey
|
|
||||||
import com.twitter.graph_feature_service.worker.util.MutualFollowPartialValueGraph
|
|
||||||
import com.twitter.inject.TwitterModule
|
|
||||||
import com.twitter.inject.annotations.Flag
|
|
||||||
import com.twitter.util.Timer
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
object GraphContainerProviderModule extends TwitterModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
def provideAutoUpdatingGraphs(
|
|
||||||
@Flag(WorkerFlagNames.HdfsCluster) hdfsCluster: String,
|
|
||||||
@Flag(WorkerFlagNames.HdfsClusterUrl) hdfsClusterUrl: String,
|
|
||||||
@Flag(WorkerFlagNames.ShardId) shardId: Int
|
|
||||||
)(
|
|
||||||
implicit statsReceiver: StatsReceiver,
|
|
||||||
timer: Timer
|
|
||||||
): GraphContainer = {
|
|
||||||
|
|
||||||
// NOTE that we do not load some the graphs for saving RAM at this moment.
|
|
||||||
val enabledGraphPaths: Map[GraphKey, String] =
|
|
||||||
Map(
|
|
||||||
FollowingPartialValueGraph -> FollowOutValPath,
|
|
||||||
FollowedByPartialValueGraph -> FollowInValPath
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only allow one graph to update at the same time.
|
|
||||||
val sharedSemaphore = new AsyncSemaphore(1)
|
|
||||||
|
|
||||||
val graphs: Map[GraphKey, AutoUpdatingGraph] =
|
|
||||||
enabledGraphPaths.map {
|
|
||||||
case (graphKey, path) =>
|
|
||||||
graphKey -> AutoUpdatingGraph(
|
|
||||||
dataPath = getHdfsPath(path),
|
|
||||||
hdfsCluster = hdfsCluster,
|
|
||||||
hdfsClusterUrl = hdfsClusterUrl,
|
|
||||||
shard = shardId,
|
|
||||||
minimumSizeForCompleteGraph = 1e6.toLong,
|
|
||||||
sharedSemaphore = Some(sharedSemaphore)
|
|
||||||
)(
|
|
||||||
statsReceiver
|
|
||||||
.scope("graphs")
|
|
||||||
.scope(graphKey.getClass.getSimpleName),
|
|
||||||
timer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
util.GraphContainer(graphs)
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,33 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.modules
|
|
||||||
|
|
||||||
import com.twitter.inject.TwitterModule
|
|
||||||
|
|
||||||
object WorkerFlagNames {
|
|
||||||
final val ServiceRole = "service.role"
|
|
||||||
final val ServiceEnv = "service.env"
|
|
||||||
final val ShardId = "service.shardId"
|
|
||||||
final val NumShards = "service.numShards"
|
|
||||||
final val HdfsCluster = "service.hdfsCluster"
|
|
||||||
final val HdfsClusterUrl = "service.hdfsClusterUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes references to the flag values defined in the aurora.deploy file.
|
|
||||||
* To check what the flag values are initialized in runtime, search FlagsModule in stdout
|
|
||||||
*/
|
|
||||||
object WorkerFlagModule extends TwitterModule {
|
|
||||||
|
|
||||||
import WorkerFlagNames._
|
|
||||||
|
|
||||||
flag[Int](ShardId, "Shard Id")
|
|
||||||
|
|
||||||
flag[Int](NumShards, "Num of Graph Shards")
|
|
||||||
|
|
||||||
flag[String](ServiceRole, "Service Role")
|
|
||||||
|
|
||||||
flag[String](ServiceEnv, "Service Env")
|
|
||||||
|
|
||||||
flag[String](HdfsCluster, "Hdfs cluster to download graph files from")
|
|
||||||
|
|
||||||
flag[String](HdfsClusterUrl, "Hdfs cluster url to download graph files from")
|
|
||||||
}
|
|
Binary file not shown.
@ -1,69 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.util
|
|
||||||
|
|
||||||
import com.twitter.bijection.Injection
|
|
||||||
import com.twitter.concurrent.AsyncSemaphore
|
|
||||||
import com.twitter.conversions.DurationOps._
|
|
||||||
import com.twitter.finagle.stats.StatsReceiver
|
|
||||||
import com.twitter.frigate.common.constdb_util.{
|
|
||||||
AutoUpdatingReadOnlyGraph,
|
|
||||||
ConstDBImporter,
|
|
||||||
Injections
|
|
||||||
}
|
|
||||||
import com.twitter.graph_feature_service.common.Configs
|
|
||||||
import com.twitter.util.{Duration, Future, Timer}
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param dataPath the path to the data on HDFS
|
|
||||||
* @param hdfsCluster cluster where we check for updates and download graph files from
|
|
||||||
* @param hdfsClusterUrl url to HDFS cluster
|
|
||||||
* @param shard The shard of the graph to download
|
|
||||||
* @param minimumSizeForCompleteGraph minimumSize for complete graph - otherwise we don't load it
|
|
||||||
* @param updateIntervalMin The interval after which the first update is tried and the interval between such updates
|
|
||||||
* @param updateIntervalMax the maximum time before an update is triggered
|
|
||||||
* @param deleteInterval The interval after which older data is deleted from disk
|
|
||||||
* @param sharedSemaphore The semaphore controls the number of graph loads at same time on the instance.
|
|
||||||
*/
|
|
||||||
case class AutoUpdatingGraph(
|
|
||||||
dataPath: String,
|
|
||||||
hdfsCluster: String,
|
|
||||||
hdfsClusterUrl: String,
|
|
||||||
shard: Int,
|
|
||||||
minimumSizeForCompleteGraph: Long,
|
|
||||||
updateIntervalMin: Duration = 1.hour,
|
|
||||||
updateIntervalMax: Duration = 12.hours,
|
|
||||||
deleteInterval: Duration = 2.seconds,
|
|
||||||
sharedSemaphore: Option[AsyncSemaphore] = None
|
|
||||||
)(
|
|
||||||
implicit statsReceiver: StatsReceiver,
|
|
||||||
timer: Timer)
|
|
||||||
extends AutoUpdatingReadOnlyGraph[Long, ByteBuffer](
|
|
||||||
hdfsCluster,
|
|
||||||
hdfsClusterUrl,
|
|
||||||
shard,
|
|
||||||
minimumSizeForCompleteGraph,
|
|
||||||
updateIntervalMin,
|
|
||||||
updateIntervalMax,
|
|
||||||
deleteInterval,
|
|
||||||
sharedSemaphore
|
|
||||||
)
|
|
||||||
with ConstDBImporter[Long, ByteBuffer] {
|
|
||||||
|
|
||||||
override def numGraphShards: Int = Configs.NumGraphShards
|
|
||||||
|
|
||||||
override def basePath: String = dataPath
|
|
||||||
|
|
||||||
override val keyInj: Injection[Long, ByteBuffer] = Injections.long2Varint
|
|
||||||
|
|
||||||
override val valueInj: Injection[ByteBuffer, ByteBuffer] = Injection.identity
|
|
||||||
|
|
||||||
override def get(targetId: Long): Future[Option[ByteBuffer]] =
|
|
||||||
super
|
|
||||||
.get(targetId)
|
|
||||||
.map { res =>
|
|
||||||
res.foreach(r => arraySizeStat.add(r.remaining()))
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arraySizeStat = stats.scope("get").stat("size")
|
|
||||||
}
|
|
Binary file not shown.
@ -1,14 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.util
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.EdgeType
|
|
||||||
|
|
||||||
sealed trait GfsQuery {
|
|
||||||
def edgeType: EdgeType
|
|
||||||
def userId: Long
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for edges for any users to users in local partition.
|
|
||||||
*/
|
|
||||||
case class ToPartialQuery(edgeType: EdgeType, userId: Long) extends GfsQuery
|
|
||||||
|
|
Binary file not shown.
@ -1,19 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.util
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.EdgeType
|
|
||||||
import com.twitter.util.Future
|
|
||||||
|
|
||||||
case class GraphContainer(
|
|
||||||
graphs: Map[GraphKey, AutoUpdatingGraph]) {
|
|
||||||
|
|
||||||
final val toPartialMap: Map[EdgeType, AutoUpdatingGraph] =
|
|
||||||
graphs.collect {
|
|
||||||
case (partialValueGraph: PartialValueGraph, graph) =>
|
|
||||||
partialValueGraph.edgeType -> graph
|
|
||||||
}
|
|
||||||
|
|
||||||
// load all the graphs from constantDB format to memory
|
|
||||||
def warmup: Future[Unit] = {
|
|
||||||
Future.collect(graphs.mapValues(_.warmup())).unit
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,32 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.util
|
|
||||||
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.EdgeType
|
|
||||||
import com.twitter.graph_feature_service.thriftscala.EdgeType._
|
|
||||||
|
|
||||||
sealed trait GraphKey {
|
|
||||||
|
|
||||||
def edgeType: EdgeType
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed trait PartialValueGraph extends GraphKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Follow Graphs
|
|
||||||
*/
|
|
||||||
object FollowingPartialValueGraph extends PartialValueGraph {
|
|
||||||
|
|
||||||
override def edgeType: EdgeType = Following
|
|
||||||
}
|
|
||||||
|
|
||||||
object FollowedByPartialValueGraph extends PartialValueGraph {
|
|
||||||
|
|
||||||
override def edgeType: EdgeType = FollowedBy
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutual Follow Graphs
|
|
||||||
*/
|
|
||||||
object MutualFollowPartialValueGraph extends PartialValueGraph {
|
|
||||||
|
|
||||||
override def edgeType: EdgeType = MutualFollow
|
|
||||||
}
|
|
Binary file not shown.
@ -1,16 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.worker.util
|
|
||||||
|
|
||||||
//These classes are to help the GraphContainer choose the right data structure to answer queries
|
|
||||||
sealed trait GraphType
|
|
||||||
|
|
||||||
object FollowGraph extends GraphType
|
|
||||||
|
|
||||||
object FavoriteGraph extends GraphType
|
|
||||||
|
|
||||||
object RetweetGraph extends GraphType
|
|
||||||
|
|
||||||
object ReplyGraph extends GraphType
|
|
||||||
|
|
||||||
object MentionGraph extends GraphType
|
|
||||||
|
|
||||||
object MutualFollowGraph extends GraphType
|
|
@ -1,66 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
platform = "java8",
|
|
||||||
tags = [
|
|
||||||
"bazel-compatible",
|
|
||||||
"bazel-only",
|
|
||||||
],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/com/twitter/bijection:core",
|
|
||||||
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/constdb_util",
|
|
||||||
"graph-feature-service/src/main/scala/com/twitter/graph_feature_service/common",
|
|
||||||
"src/scala/com/twitter/interaction_graph/scio/agg_all:interaction_graph_history_aggregated_edge_snapshot-scala",
|
|
||||||
"src/scala/com/twitter/interaction_graph/scio/ml/scores:real_graph_in_scores-scala",
|
|
||||||
"src/scala/com/twitter/pluck/source/user_audits:user_audit_final-scala",
|
|
||||||
"src/scala/com/twitter/scalding_internal/dalv2",
|
|
||||||
"src/scala/com/twitter/scalding_internal/job",
|
|
||||||
"src/scala/com/twitter/scalding_internal/job/analytics_batch",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
scalding_job(
|
|
||||||
name = "graph_feature_service_adhoc_job",
|
|
||||||
main = "com.twitter.graph_feature_service.scalding.GraphFeatureServiceAdhocApp",
|
|
||||||
args = [
|
|
||||||
"--date 2022-10-24",
|
|
||||||
],
|
|
||||||
config = [
|
|
||||||
("hadoop.map.jvm.total-memory", "3072m"),
|
|
||||||
("hadoop.reduce.jvm.total-memory", "3072m"),
|
|
||||||
("hadoop.submitter.jvm.total-memory", "5120m"),
|
|
||||||
("submitter.tier", "preemptible"),
|
|
||||||
],
|
|
||||||
contact = "recos-platform-alerts@twitter.com",
|
|
||||||
hadoop_cluster = "atla-proc",
|
|
||||||
hadoop_properties = [("mapreduce.job.hdfs-servers", "/atla/proc/user/cassowary")],
|
|
||||||
platform = "java8",
|
|
||||||
role = "cassowary",
|
|
||||||
runtime_platform = "java8",
|
|
||||||
tags = [
|
|
||||||
"bazel-compatible:migrated",
|
|
||||||
"bazel-only",
|
|
||||||
],
|
|
||||||
dependencies = [":scalding"],
|
|
||||||
)
|
|
||||||
|
|
||||||
scalding_job(
|
|
||||||
name = "graph_feature_service_daily_job",
|
|
||||||
main = "com.twitter.graph_feature_service.scalding.GraphFeatureServiceScheduledApp",
|
|
||||||
config = [
|
|
||||||
("hadoop.map.jvm.total-memory", "3072m"),
|
|
||||||
("hadoop.reduce.jvm.total-memory", "3072m"),
|
|
||||||
("hadoop.submitter.jvm.total-memory", "5120m"),
|
|
||||||
("submitter.tier", "preemptible"),
|
|
||||||
],
|
|
||||||
contact = "recos-platform-alerts@twitter.com",
|
|
||||||
cron = "01,31 * * * *",
|
|
||||||
hadoop_cluster = "atla-proc",
|
|
||||||
hadoop_properties = [("mapreduce.job.hdfs-servers", "/atla/proc/user/cassowary")],
|
|
||||||
platform = "java8",
|
|
||||||
role = "cassowary",
|
|
||||||
runtime_platform = "java8",
|
|
||||||
tags = [
|
|
||||||
"bazel-compatible:migrated",
|
|
||||||
"bazel-only",
|
|
||||||
],
|
|
||||||
dependencies = [":scalding"],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,9 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.scalding
|
|
||||||
|
|
||||||
case class EdgeFeature(
|
|
||||||
realGraphScore: Float,
|
|
||||||
followScore: Option[Float] = None,
|
|
||||||
mutualFollowScore: Option[Float] = None,
|
|
||||||
favoriteScore: Option[Float] = None,
|
|
||||||
retweetScore: Option[Float] = None,
|
|
||||||
mentionScore: Option[Float] = None)
|
|
Binary file not shown.
@ -1,85 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.scalding
|
|
||||||
|
|
||||||
import com.twitter.scalding._
|
|
||||||
import com.twitter.scalding_internal.job.TwitterExecutionApp
|
|
||||||
import com.twitter.scalding_internal.job.analytics_batch.{
|
|
||||||
AnalyticsBatchExecution,
|
|
||||||
AnalyticsBatchExecutionArgs,
|
|
||||||
BatchDescription,
|
|
||||||
BatchFirstTime,
|
|
||||||
BatchIncrement,
|
|
||||||
TwitterScheduledExecutionApp
|
|
||||||
}
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Each job only needs to implement this runOnDateRange() function. It makes it easier for testing.
|
|
||||||
*/
|
|
||||||
trait GraphFeatureServiceBaseJob {
|
|
||||||
implicit val timeZone: TimeZone = DateOps.UTC
|
|
||||||
implicit val dateParser: DateParser = DateParser.default
|
|
||||||
|
|
||||||
def runOnDateRange(
|
|
||||||
enableValueGraphs: Option[Boolean] = None,
|
|
||||||
enableKeyGraphs: Option[Boolean] = None
|
|
||||||
)(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
timeZone: TimeZone,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): Execution[Unit]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print customized counters in the log
|
|
||||||
*/
|
|
||||||
def printerCounters[T](execution: Execution[T]): Execution[Unit] = {
|
|
||||||
execution.getCounters
|
|
||||||
.flatMap {
|
|
||||||
case (_, counters) =>
|
|
||||||
counters.toMap.toSeq
|
|
||||||
.sortBy(e => (e._1.group, e._1.counter))
|
|
||||||
.foreach {
|
|
||||||
case (statKey, value) =>
|
|
||||||
println(s"${statKey.group}\t${statKey.counter}\t$value")
|
|
||||||
}
|
|
||||||
Execution.unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trait that wraps things about adhoc jobs.
|
|
||||||
*/
|
|
||||||
trait GraphFeatureServiceAdhocBaseApp extends TwitterExecutionApp with GraphFeatureServiceBaseJob {
|
|
||||||
override def job: Execution[Unit] = Execution.withId { implicit uniqueId =>
|
|
||||||
Execution.getArgs.flatMap { args: Args =>
|
|
||||||
implicit val dateRange: DateRange = DateRange.parse(args.list("date"))(timeZone, dateParser)
|
|
||||||
printerCounters(runOnDateRange())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trait that wraps things about scheduled jobs.
|
|
||||||
*
|
|
||||||
* A new daily app only needs to declare the starting date.
|
|
||||||
*/
|
|
||||||
trait GraphFeatureServiceScheduledBaseApp
|
|
||||||
extends TwitterScheduledExecutionApp
|
|
||||||
with GraphFeatureServiceBaseJob {
|
|
||||||
|
|
||||||
def firstTime: RichDate // for example: RichDate("2018-02-21")
|
|
||||||
|
|
||||||
def batchIncrement: Duration = Days(1)
|
|
||||||
|
|
||||||
override def scheduledJob: Execution[Unit] = Execution.withId { implicit uniqueId =>
|
|
||||||
val analyticsArgs = AnalyticsBatchExecutionArgs(
|
|
||||||
batchDesc = BatchDescription(getClass.getName),
|
|
||||||
firstTime = BatchFirstTime(firstTime),
|
|
||||||
batchIncrement = BatchIncrement(batchIncrement)
|
|
||||||
)
|
|
||||||
|
|
||||||
AnalyticsBatchExecution(analyticsArgs) { implicit dateRange =>
|
|
||||||
printerCounters(runOnDateRange())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,52 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.scalding
|
|
||||||
|
|
||||||
import com.twitter.scalding.DateRange
|
|
||||||
import com.twitter.scalding.Execution
|
|
||||||
import com.twitter.scalding.RichDate
|
|
||||||
import com.twitter.scalding.UniqueID
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.TimeZone
|
|
||||||
import sun.util.calendar.BaseCalendar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To launch an adhoc run:
|
|
||||||
*
|
|
||||||
scalding remote run --target graph-feature-service/src/main/scalding/com/twitter/graph_feature_service/scalding:graph_feature_service_adhoc_job
|
|
||||||
*/
|
|
||||||
object GraphFeatureServiceAdhocApp
|
|
||||||
extends GraphFeatureServiceMainJob
|
|
||||||
with GraphFeatureServiceAdhocBaseApp {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To schedule the job, upload the workflows config (only required for the first time and subsequent config changes):
|
|
||||||
* scalding workflow upload --jobs graph-feature-service/src/main/scalding/com/twitter/graph_feature_service/scalding:graph_feature_service_daily_job --autoplay --build-cron-schedule "20 23 1 * *"
|
|
||||||
* You can then build from the UI by clicking "Build" and pasting in your remote branch, or leave it empty if you're redeploying from master.
|
|
||||||
* The workflows config above should automatically trigger once each month.
|
|
||||||
*/
|
|
||||||
object GraphFeatureServiceScheduledApp
|
|
||||||
extends GraphFeatureServiceMainJob
|
|
||||||
with GraphFeatureServiceScheduledBaseApp {
|
|
||||||
override def firstTime: RichDate = RichDate("2018-05-18")
|
|
||||||
|
|
||||||
override def runOnDateRange(
|
|
||||||
enableValueGraphs: Option[Boolean],
|
|
||||||
enableKeyGraphs: Option[Boolean]
|
|
||||||
)(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
timeZone: TimeZone,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): Execution[Unit] = {
|
|
||||||
// Only run the value Graphs on Tuesday, Thursday, Saturday
|
|
||||||
val overrideEnableValueGraphs = {
|
|
||||||
val dayOfWeek = dateRange.start.toCalendar.get(Calendar.DAY_OF_WEEK)
|
|
||||||
dayOfWeek == BaseCalendar.TUESDAY |
|
|
||||||
dayOfWeek == BaseCalendar.THURSDAY |
|
|
||||||
dayOfWeek == BaseCalendar.SATURDAY
|
|
||||||
}
|
|
||||||
|
|
||||||
super.runOnDateRange(
|
|
||||||
Some(true),
|
|
||||||
Some(false) // disable key Graphs since we are not using them in production
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,297 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.scalding
|
|
||||||
|
|
||||||
import com.twitter.bijection.Injection
|
|
||||||
import com.twitter.frigate.common.constdb_util.Injections
|
|
||||||
import com.twitter.frigate.common.constdb_util.ScaldingUtil
|
|
||||||
import com.twitter.graph_feature_service.common.Configs
|
|
||||||
import com.twitter.graph_feature_service.common.Configs._
|
|
||||||
import com.twitter.interaction_graph.scio.agg_all.InteractionGraphHistoryAggregatedEdgeSnapshotScalaDataset
|
|
||||||
import com.twitter.interaction_graph.scio.ml.scores.RealGraphInScoresScalaDataset
|
|
||||||
import com.twitter.interaction_graph.thriftscala.FeatureName
|
|
||||||
import com.twitter.interaction_graph.thriftscala.{EdgeFeature => TEdgeFeature}
|
|
||||||
import com.twitter.pluck.source.user_audits.UserAuditFinalScalaDataset
|
|
||||||
import com.twitter.scalding.DateRange
|
|
||||||
import com.twitter.scalding.Days
|
|
||||||
import com.twitter.scalding.Execution
|
|
||||||
import com.twitter.scalding.Stat
|
|
||||||
import com.twitter.scalding.UniqueID
|
|
||||||
import com.twitter.scalding.typed.TypedPipe
|
|
||||||
import com.twitter.scalding_internal.dalv2.DAL
|
|
||||||
import com.twitter.scalding_internal.dalv2.remote_access.AllowCrossClusterSameDC
|
|
||||||
import com.twitter.scalding_internal.multiformat.format.keyval.KeyVal
|
|
||||||
import com.twitter.util.Time
|
|
||||||
import com.twitter.wtf.candidate.thriftscala.CandidateSeq
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
trait GraphFeatureServiceMainJob extends GraphFeatureServiceBaseJob {
|
|
||||||
|
|
||||||
// keeping hdfsPath as a separate variable in order to override it in unit tests
|
|
||||||
protected val hdfsPath: String = BaseHdfsPath
|
|
||||||
|
|
||||||
protected def getShardIdForUser(userId: Long): Int = shardForUser(userId)
|
|
||||||
|
|
||||||
protected implicit val keyInj: Injection[Long, ByteBuffer] = Injections.long2Varint
|
|
||||||
|
|
||||||
protected implicit val valueInj: Injection[Long, ByteBuffer] = Injections.long2ByteBuffer
|
|
||||||
|
|
||||||
protected val bufferSize: Int = 1 << 26
|
|
||||||
|
|
||||||
protected val maxNumKeys: Int = 1 << 24
|
|
||||||
|
|
||||||
protected val numReducers: Int = NumGraphShards
|
|
||||||
|
|
||||||
protected val outputStreamBufferSize: Int = 1 << 26
|
|
||||||
|
|
||||||
protected final val shardingByKey = { (k: Long, _: Long) =>
|
|
||||||
getShardIdForUser(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final val shardingByValue = { (_: Long, v: Long) =>
|
|
||||||
getShardIdForUser(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def writeGraphToDB(
|
|
||||||
graph: TypedPipe[(Long, Long)],
|
|
||||||
shardingFunction: (Long, Long) => Int,
|
|
||||||
path: String
|
|
||||||
)(
|
|
||||||
implicit dateRange: DateRange
|
|
||||||
): Execution[TypedPipe[(Int, Unit)]] = {
|
|
||||||
ScaldingUtil
|
|
||||||
.writeConstDB[Long, Long](
|
|
||||||
graph.withDescription(s"sharding $path"),
|
|
||||||
shardingFunction,
|
|
||||||
shardId =>
|
|
||||||
getTimedHdfsShardPath(
|
|
||||||
shardId,
|
|
||||||
getHdfsPath(path, Some(hdfsPath)),
|
|
||||||
Time.fromMilliseconds(dateRange.end.timestamp)
|
|
||||||
),
|
|
||||||
Int.MaxValue,
|
|
||||||
bufferSize,
|
|
||||||
maxNumKeys,
|
|
||||||
numReducers,
|
|
||||||
outputStreamBufferSize
|
|
||||||
)(
|
|
||||||
keyInj,
|
|
||||||
valueInj,
|
|
||||||
Ordering[(Long, Long)]
|
|
||||||
)
|
|
||||||
.forceToDiskExecution
|
|
||||||
}
|
|
||||||
|
|
||||||
def extractFeature(
|
|
||||||
featureList: Seq[TEdgeFeature],
|
|
||||||
featureName: FeatureName
|
|
||||||
): Option[Float] = {
|
|
||||||
featureList
|
|
||||||
.find(_.name == featureName)
|
|
||||||
.map(_.tss.ewma.toFloat)
|
|
||||||
.filter(_ > 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to extract a subgraph (e.g., follow graph) from real graph and take top K by real graph
|
|
||||||
* weight.
|
|
||||||
*
|
|
||||||
* @param input input real graph
|
|
||||||
* @param edgeFilter filter function to only get the edges needed (e.g., only follow edges)
|
|
||||||
* @param counter counter
|
|
||||||
* @return a subgroup that contains topK, e.g., follow graph for each user.
|
|
||||||
*/
|
|
||||||
private def getSubGraph(
|
|
||||||
input: TypedPipe[(Long, Long, EdgeFeature)],
|
|
||||||
edgeFilter: EdgeFeature => Boolean,
|
|
||||||
counter: Stat
|
|
||||||
): TypedPipe[(Long, Long)] = {
|
|
||||||
input
|
|
||||||
.filter(c => edgeFilter(c._3))
|
|
||||||
.map {
|
|
||||||
case (srcId, destId, features) =>
|
|
||||||
(srcId, (destId, features.realGraphScore))
|
|
||||||
}
|
|
||||||
.group
|
|
||||||
// auto reducer estimation only allocates 15 reducers, so setting an explicit number here
|
|
||||||
.withReducers(2000)
|
|
||||||
.sortedReverseTake(TopKRealGraph)(Ordering.by(_._2))
|
|
||||||
.flatMap {
|
|
||||||
case (srcId, topKNeighbors) =>
|
|
||||||
counter.inc()
|
|
||||||
topKNeighbors.map {
|
|
||||||
case (destId, _) =>
|
|
||||||
(srcId, destId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getMauIds()(implicit dateRange: DateRange, uniqueID: UniqueID): TypedPipe[Long] = {
|
|
||||||
val numMAUs = Stat("NUM_MAUS")
|
|
||||||
val uniqueMAUs = Stat("UNIQUE_MAUS")
|
|
||||||
|
|
||||||
DAL
|
|
||||||
.read(UserAuditFinalScalaDataset)
|
|
||||||
.withRemoteReadPolicy(AllowCrossClusterSameDC)
|
|
||||||
.toTypedPipe
|
|
||||||
.collect {
|
|
||||||
case user_audit if user_audit.isValid =>
|
|
||||||
numMAUs.inc()
|
|
||||||
user_audit.userId
|
|
||||||
}
|
|
||||||
.distinct
|
|
||||||
.map { u =>
|
|
||||||
uniqueMAUs.inc()
|
|
||||||
u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getRealGraphWithMAUOnly(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
timeZone: TimeZone,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): TypedPipe[(Long, Long, EdgeFeature)] = {
|
|
||||||
val numMAUs = Stat("NUM_MAUS")
|
|
||||||
val uniqueMAUs = Stat("UNIQUE_MAUS")
|
|
||||||
|
|
||||||
val monthlyActiveUsers = DAL
|
|
||||||
.read(UserAuditFinalScalaDataset)
|
|
||||||
.withRemoteReadPolicy(AllowCrossClusterSameDC)
|
|
||||||
.toTypedPipe
|
|
||||||
.collect {
|
|
||||||
case user_audit if user_audit.isValid =>
|
|
||||||
numMAUs.inc()
|
|
||||||
user_audit.userId
|
|
||||||
}
|
|
||||||
.distinct
|
|
||||||
.map { u =>
|
|
||||||
uniqueMAUs.inc()
|
|
||||||
u
|
|
||||||
}
|
|
||||||
.asKeys
|
|
||||||
|
|
||||||
val realGraphAggregates = DAL
|
|
||||||
.readMostRecentSnapshot(
|
|
||||||
InteractionGraphHistoryAggregatedEdgeSnapshotScalaDataset,
|
|
||||||
dateRange.embiggen(Days(5)))
|
|
||||||
.withRemoteReadPolicy(AllowCrossClusterSameDC)
|
|
||||||
.toTypedPipe
|
|
||||||
.map { edge =>
|
|
||||||
val featureList = edge.features
|
|
||||||
val edgeFeature = EdgeFeature(
|
|
||||||
edge.weight.getOrElse(0.0).toFloat,
|
|
||||||
extractFeature(featureList, FeatureName.NumMutualFollows),
|
|
||||||
extractFeature(featureList, FeatureName.NumFavorites),
|
|
||||||
extractFeature(featureList, FeatureName.NumRetweets),
|
|
||||||
extractFeature(featureList, FeatureName.NumMentions)
|
|
||||||
)
|
|
||||||
(edge.sourceId, (edge.destinationId, edgeFeature))
|
|
||||||
}
|
|
||||||
.join(monthlyActiveUsers)
|
|
||||||
.map {
|
|
||||||
case (srcId, ((destId, feature), _)) =>
|
|
||||||
(destId, (srcId, feature))
|
|
||||||
}
|
|
||||||
.join(monthlyActiveUsers)
|
|
||||||
.map {
|
|
||||||
case (destId, ((srcId, feature), _)) =>
|
|
||||||
(srcId, destId, feature)
|
|
||||||
}
|
|
||||||
realGraphAggregates
|
|
||||||
}
|
|
||||||
|
|
||||||
def getTopKFollowGraph(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
timeZone: TimeZone,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): TypedPipe[(Long, Long)] = {
|
|
||||||
val followGraphMauStat = Stat("NumFollowEdges_MAU")
|
|
||||||
val mau: TypedPipe[Long] = getMauIds()
|
|
||||||
DAL
|
|
||||||
.readMostRecentSnapshot(RealGraphInScoresScalaDataset, dateRange.embiggen(Days(7)))
|
|
||||||
.withRemoteReadPolicy(AllowCrossClusterSameDC)
|
|
||||||
.toTypedPipe
|
|
||||||
.groupBy(_.key)
|
|
||||||
.join(mau.asKeys)
|
|
||||||
.withDescription("filtering srcId by mau")
|
|
||||||
.flatMap {
|
|
||||||
case (_, (KeyVal(srcId, CandidateSeq(candidates)), _)) =>
|
|
||||||
followGraphMauStat.inc()
|
|
||||||
val topK = candidates.sortBy(-_.score).take(TopKRealGraph)
|
|
||||||
topK.map { c => (srcId, c.userId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def runOnDateRange(
|
|
||||||
enableValueGraphs: Option[Boolean],
|
|
||||||
enableKeyGraphs: Option[Boolean]
|
|
||||||
)(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
timeZone: TimeZone,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): Execution[Unit] = {
|
|
||||||
|
|
||||||
val processValueGraphs = enableValueGraphs.getOrElse(Configs.EnableValueGraphs)
|
|
||||||
val processKeyGraphs = enableKeyGraphs.getOrElse(Configs.EnableKeyGraphs)
|
|
||||||
|
|
||||||
if (!processKeyGraphs && !processValueGraphs) {
|
|
||||||
// Skip the batch job
|
|
||||||
Execution.unit
|
|
||||||
} else {
|
|
||||||
// val favoriteGraphStat = Stat("NumFavoriteEdges")
|
|
||||||
// val retweetGraphStat = Stat("NumRetweetEdges")
|
|
||||||
// val mentionGraphStat = Stat("NumMentionEdges")
|
|
||||||
|
|
||||||
// val realGraphAggregates = getRealGraphWithMAUOnly
|
|
||||||
|
|
||||||
val followGraph = getTopKFollowGraph
|
|
||||||
// val mutualFollowGraph = followGraph.asKeys.join(followGraph.swap.asKeys).keys
|
|
||||||
|
|
||||||
// val favoriteGraph =
|
|
||||||
// getSubGraph(realGraphAggregates, _.favoriteScore.isDefined, favoriteGraphStat)
|
|
||||||
|
|
||||||
// val retweetGraph =
|
|
||||||
// getSubGraph(realGraphAggregates, _.retweetScore.isDefined, retweetGraphStat)
|
|
||||||
|
|
||||||
// val mentionGraph =
|
|
||||||
// getSubGraph(realGraphAggregates, _.mentionScore.isDefined, mentionGraphStat)
|
|
||||||
|
|
||||||
val writeValDataSetExecutions = if (processValueGraphs) {
|
|
||||||
Seq(
|
|
||||||
(followGraph, shardingByValue, FollowOutValPath),
|
|
||||||
(followGraph.swap, shardingByValue, FollowInValPath)
|
|
||||||
// (mutualFollowGraph, shardingByValue, MutualFollowValPath),
|
|
||||||
// (favoriteGraph, shardingByValue, FavoriteOutValPath),
|
|
||||||
// (favoriteGraph.swap, shardingByValue, FavoriteInValPath),
|
|
||||||
// (retweetGraph, shardingByValue, RetweetOutValPath),
|
|
||||||
// (retweetGraph.swap, shardingByValue, RetweetInValPath),
|
|
||||||
// (mentionGraph, shardingByValue, MentionOutValPath),
|
|
||||||
// (mentionGraph.swap, shardingByValue, MentionInValPath)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Seq.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
val writeKeyDataSetExecutions = if (processKeyGraphs) {
|
|
||||||
Seq(
|
|
||||||
(followGraph, shardingByKey, FollowOutKeyPath),
|
|
||||||
(followGraph.swap, shardingByKey, FollowInKeyPath)
|
|
||||||
// (favoriteGraph, shardingByKey, FavoriteOutKeyPath),
|
|
||||||
// (favoriteGraph.swap, shardingByKey, FavoriteInKeyPath),
|
|
||||||
// (retweetGraph, shardingByKey, RetweetOutKeyPath),
|
|
||||||
// (retweetGraph.swap, shardingByKey, RetweetInKeyPath),
|
|
||||||
// (mentionGraph, shardingByKey, MentionOutKeyPath),
|
|
||||||
// (mentionGraph.swap, shardingByKey, MentionInKeyPath),
|
|
||||||
// (mutualFollowGraph, shardingByKey, MutualFollowKeyPath)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Seq.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
Execution
|
|
||||||
.sequence((writeValDataSetExecutions ++ writeKeyDataSetExecutions).map {
|
|
||||||
case (graph, shardingMethod, path) =>
|
|
||||||
writeGraphToDB(graph, shardingMethod, path)
|
|
||||||
}).unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
platform = "java8",
|
|
||||||
tags = ["bazel-only"],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/com/twitter/bijection:core",
|
|
||||||
"3rdparty/jvm/com/twitter/bijection:scrooge",
|
|
||||||
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/constdb_util",
|
|
||||||
"src/java/com/twitter/ml/api:api-base",
|
|
||||||
"src/scala/com/twitter/ml/api:api-base",
|
|
||||||
"src/scala/com/twitter/scalding_internal/job",
|
|
||||||
"src/scala/com/twitter/scalding_internal/job/analytics_batch",
|
|
||||||
"src/thrift/com/twitter/ml/api:data-java",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
hadoop_binary(
|
|
||||||
name = "gfs_random_request-adhoc",
|
|
||||||
main = "com.twitter.graph_feature_service.scalding.adhoc.RandomRequestGenerationApp",
|
|
||||||
platform = "java8",
|
|
||||||
runtime_platform = "java8",
|
|
||||||
tags = [
|
|
||||||
"bazel-compatible",
|
|
||||||
"bazel-compatible:migrated",
|
|
||||||
"bazel-only",
|
|
||||||
],
|
|
||||||
dependencies = [":adhoc"],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,77 +0,0 @@
|
|||||||
package com.twitter.graph_feature_service.scalding.adhoc
|
|
||||||
|
|
||||||
import com.twitter.bijection.Injection
|
|
||||||
import com.twitter.frigate.common.constdb_util.Injections
|
|
||||||
import com.twitter.ml.api.Feature.Discrete
|
|
||||||
import com.twitter.ml.api.{DailySuffixFeatureSource, DataSetPipe, RichDataRecord}
|
|
||||||
import com.twitter.scalding._
|
|
||||||
import com.twitter.scalding_internal.job.TwitterExecutionApp
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
object RandomRequestGenerationJob {
|
|
||||||
implicit val timeZone: TimeZone = DateOps.UTC
|
|
||||||
implicit val dateParser: DateParser = DateParser.default
|
|
||||||
|
|
||||||
val timelineRecapDataSetPath: String =
|
|
||||||
"/atla/proc2/user/timelines/processed/suggests/recap/data_records"
|
|
||||||
|
|
||||||
val USER_ID = new Discrete("meta.user_id")
|
|
||||||
val AUTHOR_ID = new Discrete("meta.author_id")
|
|
||||||
|
|
||||||
val timelineRecapOutPutPath: String = "/user/cassowary/gfs/adhoc/timeline_data"
|
|
||||||
|
|
||||||
implicit val inj: Injection[Long, ByteBuffer] = Injections.long2Varint
|
|
||||||
|
|
||||||
def run(
|
|
||||||
dataSetPath: String,
|
|
||||||
outPutPath: String,
|
|
||||||
numOfPairsToTake: Int
|
|
||||||
)(
|
|
||||||
implicit dateRange: DateRange,
|
|
||||||
uniqueID: UniqueID
|
|
||||||
): Execution[Unit] = {
|
|
||||||
|
|
||||||
val NumUserAuthorPairs = Stat("NumUserAuthorPairs")
|
|
||||||
|
|
||||||
val dataSet: DataSetPipe = DailySuffixFeatureSource(dataSetPath).read
|
|
||||||
|
|
||||||
val userAuthorPairs: TypedPipe[(Long, Long)] = dataSet.records.map { record =>
|
|
||||||
val richRecord = new RichDataRecord(record, dataSet.featureContext)
|
|
||||||
|
|
||||||
val userId = richRecord.getFeatureValue(USER_ID)
|
|
||||||
val authorId = richRecord.getFeatureValue(AUTHOR_ID)
|
|
||||||
NumUserAuthorPairs.inc()
|
|
||||||
(userId, authorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
userAuthorPairs
|
|
||||||
.limit(numOfPairsToTake)
|
|
||||||
.writeExecution(
|
|
||||||
TypedTsv[(Long, Long)](outPutPath)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ./bazel bundle graph-feature-service/src/main/scalding/com/twitter/graph_feature_service/scalding/adhoc:all
|
|
||||||
*
|
|
||||||
* oscar hdfs --screen --user cassowary --tee gfs_log --bundle gfs_random_request-adhoc \
|
|
||||||
--tool com.twitter.graph_feature_service.scalding.adhoc.RandomRequestGenerationApp \
|
|
||||||
-- --date 2018-08-11 \
|
|
||||||
--input /atla/proc2/user/timelines/processed/suggests/recap/data_records \
|
|
||||||
--output /user/cassowary/gfs/adhoc/timeline_data
|
|
||||||
*/
|
|
||||||
object RandomRequestGenerationApp extends TwitterExecutionApp {
|
|
||||||
import RandomRequestGenerationJob._
|
|
||||||
override def job: Execution[Unit] = Execution.withId { implicit uniqueId =>
|
|
||||||
Execution.getArgs.flatMap { args: Args =>
|
|
||||||
implicit val dateRange: DateRange = DateRange.parse(args.list("date"))(timeZone, dateParser)
|
|
||||||
run(
|
|
||||||
args.optional("input").getOrElse(timelineRecapDataSetPath),
|
|
||||||
args.optional("output").getOrElse(timelineRecapOutPutPath),
|
|
||||||
args.int("num_pairs", 3000)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
create_thrift_libraries(
|
|
||||||
base_name = "graph_feature_service_thrift",
|
|
||||||
sources = ["*.thrift"],
|
|
||||||
platform = "java8",
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
generate_languages = [
|
|
||||||
"java",
|
|
||||||
# ruby is added due to ruby dependees in timelines
|
|
||||||
"ruby",
|
|
||||||
"scala",
|
|
||||||
"strato",
|
|
||||||
],
|
|
||||||
provides_java_name = "graph_feature_service_thrift_java",
|
|
||||||
provides_scala_name = "graph_feature_service_thrift_scala",
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,123 +0,0 @@
|
|||||||
namespace java com.twitter.graph_feature_service.thriftjava
|
|
||||||
#@namespace scala com.twitter.graph_feature_service.thriftscala
|
|
||||||
#@namespace strato com.twitter.graph_feature_service.thriftscala
|
|
||||||
|
|
||||||
// edge type to differentiate different types of graphs (we can also add a lot of other types of edges)
|
|
||||||
enum EdgeType {
|
|
||||||
FOLLOWING,
|
|
||||||
FOLLOWED_BY,
|
|
||||||
FAVORITE,
|
|
||||||
FAVORITED_BY,
|
|
||||||
RETWEET,
|
|
||||||
RETWEETED_BY,
|
|
||||||
REPLY,
|
|
||||||
REPLYED_BY,
|
|
||||||
MENTION,
|
|
||||||
MENTIONED_BY,
|
|
||||||
MUTUAL_FOLLOW,
|
|
||||||
SIMILAR_TO, // more edge types (like block, report, etc.) can be supported later.
|
|
||||||
RESERVED_12,
|
|
||||||
RESERVED_13,
|
|
||||||
RESERVED_14,
|
|
||||||
RESERVED_15,
|
|
||||||
RESERVED_16,
|
|
||||||
RESERVED_17,
|
|
||||||
RESERVED_18,
|
|
||||||
RESERVED_19,
|
|
||||||
RESERVED_20
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PresetFeatureTypes {
|
|
||||||
EMPTY,
|
|
||||||
HTL_TWO_HOP,
|
|
||||||
WTF_TWO_HOP,
|
|
||||||
SQ_TWO_HOP,
|
|
||||||
RUX_TWO_HOP,
|
|
||||||
MR_TWO_HOP,
|
|
||||||
USER_TYPEAHEAD_TWO_HOP
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UserWithCount {
|
|
||||||
1: required i64 userId(personalDataType = 'UserId')
|
|
||||||
2: required i32 count
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct UserWithScore {
|
|
||||||
1: required i64 userId(personalDataType = 'UserId')
|
|
||||||
2: required double score
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
// Feature Type
|
|
||||||
// For example, to compute how many of source user's following's have favorited candidate user,
|
|
||||||
// we need to compute the intersection between source user's FOLLOWING edges, and candidate user's
|
|
||||||
// FAVORITED_BY edge. In this case, we should user FeatureType(FOLLOWING, FAVORITED_BY)
|
|
||||||
struct FeatureType {
|
|
||||||
1: required EdgeType leftEdgeType // edge type from source user
|
|
||||||
2: required EdgeType rightEdgeType // edge type from candidate user
|
|
||||||
}(persisted="true")
|
|
||||||
|
|
||||||
struct IntersectionValue {
|
|
||||||
1: required FeatureType featureType
|
|
||||||
2: optional i32 count
|
|
||||||
3: optional list<i64> intersectionIds(personalDataType = 'UserId')
|
|
||||||
4: optional i32 leftNodeDegree
|
|
||||||
5: optional i32 rightNodeDegree
|
|
||||||
}(persisted="true", hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct GfsIntersectionResult {
|
|
||||||
1: required i64 candidateUserId(personalDataType = 'UserId')
|
|
||||||
2: required list<IntersectionValue> intersectionValues
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct GfsIntersectionRequest {
|
|
||||||
1: required i64 userId(personalDataType = 'UserId')
|
|
||||||
2: required list<i64> candidateUserIds(personalDataType = 'UserId')
|
|
||||||
3: required list<FeatureType> featureTypes
|
|
||||||
4: optional i32 intersectionIdLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GfsPresetIntersectionRequest {
|
|
||||||
1: required i64 userId(personalDataType = 'UserId')
|
|
||||||
2: required list<i64> candidateUserIds(personalDataType = 'UserId')
|
|
||||||
3: required PresetFeatureTypes presetFeatureTypes
|
|
||||||
4: optional i32 intersectionIdLimit
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct GfsIntersectionResponse {
|
|
||||||
1: required list<GfsIntersectionResult> results
|
|
||||||
}
|
|
||||||
|
|
||||||
service Server {
|
|
||||||
GfsIntersectionResponse getIntersection(1: GfsIntersectionRequest request)
|
|
||||||
GfsIntersectionResponse getPresetIntersection(1: GfsPresetIntersectionRequest request)
|
|
||||||
}
|
|
||||||
|
|
||||||
###################################################################################################
|
|
||||||
## For internal usage only
|
|
||||||
###################################################################################################
|
|
||||||
struct WorkerIntersectionRequest {
|
|
||||||
1: required i64 userId(personalDataType = 'UserId')
|
|
||||||
2: required list<i64> candidateUserIds(personalDataType = 'UserId')
|
|
||||||
3: required list<FeatureType> featureTypes
|
|
||||||
4: required PresetFeatureTypes presetFeatureTypes
|
|
||||||
5: required i32 intersectionIdLimit
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct WorkerIntersectionResponse {
|
|
||||||
1: required list<list<WorkerIntersectionValue>> results
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WorkerIntersectionValue {
|
|
||||||
1: i32 count
|
|
||||||
2: i32 leftNodeDegree
|
|
||||||
3: i32 rightNodeDegree
|
|
||||||
4: list<i64> intersectionIds(personalDataType = 'UserId')
|
|
||||||
}(hasPersonalData = 'true')
|
|
||||||
|
|
||||||
struct CachedIntersectionResult {
|
|
||||||
1: required list<WorkerIntersectionValue> values
|
|
||||||
}
|
|
||||||
|
|
||||||
service Worker {
|
|
||||||
WorkerIntersectionResponse getIntersection(1: WorkerIntersectionRequest request)
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
jvm_binary(
|
|
||||||
name = "bin",
|
|
||||||
basename = "home-mixer",
|
|
||||||
main = "com.twitter.home_mixer.HomeMixerServerMain",
|
|
||||||
runtime_platform = "java11",
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/ch/qos/logback:logback-classic",
|
|
||||||
"finagle/finagle-zipkin-scribe/src/main/scala",
|
|
||||||
"finatra/inject/inject-logback/src/main/scala",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer",
|
|
||||||
"loglens/loglens-logback/src/main/scala/com/twitter/loglens/logback",
|
|
||||||
"twitter-server-internal/src/main/scala",
|
|
||||||
"twitter-server/logback-classic/src/main/scala",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Aurora Workflows build phase convention requires a jvm_app named with home-mixer-app
|
|
||||||
jvm_app(
|
|
||||||
name = "home-mixer-app",
|
|
||||||
archive = "zip",
|
|
||||||
binary = ":bin",
|
|
||||||
bundles = [
|
|
||||||
bundle(
|
|
||||||
fileset = ["config/**/*"],
|
|
||||||
owning_target = "home-mixer/config:files",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
)
|
|
BIN
home-mixer/BUILD.docx
Normal file
BIN
home-mixer/BUILD.docx
Normal file
Binary file not shown.
BIN
home-mixer/README.docx
Normal file
BIN
home-mixer/README.docx
Normal file
Binary file not shown.
@ -1,101 +0,0 @@
|
|||||||
Home Mixer
|
|
||||||
==========
|
|
||||||
|
|
||||||
Home Mixer is the main service used to construct and serve Twitter's Home Timelines. It currently
|
|
||||||
powers:
|
|
||||||
- For you - best Tweets from people you follow + recommended out-of-network content
|
|
||||||
- Following - reverse chronological Tweets from people you follow
|
|
||||||
- Lists - reverse chronological Tweets from List members
|
|
||||||
|
|
||||||
Home Mixer is built on Product Mixer, our custom Scala framework that facilitates building
|
|
||||||
feeds of content.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The For You recommendation algorithm in Home Mixer involves the following stages:
|
|
||||||
|
|
||||||
- Candidate Generation - fetch Tweets from various Candidate Sources. For example:
|
|
||||||
- Earlybird Search Index
|
|
||||||
- User Tweet Entity Graph
|
|
||||||
- Cr Mixer
|
|
||||||
- Follow Recommendations Service
|
|
||||||
- Feature Hydration
|
|
||||||
- Fetch the ~6000 features needed for ranking
|
|
||||||
- Scoring and Ranking using ML model
|
|
||||||
- Filters and Heuristics. For example:
|
|
||||||
- Author Diversity
|
|
||||||
- Content Balance (In network vs Out of Network)
|
|
||||||
- Feedback fatigue
|
|
||||||
- Deduplication / previously seen Tweets removal
|
|
||||||
- Visibility Filtering (blocked, muted authors/tweets, NSFW settings)
|
|
||||||
- Mixing - integrate Tweets with non-Tweet content
|
|
||||||
- Ads
|
|
||||||
- Who-to-follow modules
|
|
||||||
- Prompts
|
|
||||||
- Product Features and Serving
|
|
||||||
- Conversation Modules for replies
|
|
||||||
- Social Context
|
|
||||||
- Timeline Navigation
|
|
||||||
- Edited Tweets
|
|
||||||
- Feedback options
|
|
||||||
- Pagination and cursoring
|
|
||||||
- Observability and logging
|
|
||||||
- Client instructions and content marshalling
|
|
||||||
|
|
||||||
## Pipeline Structure
|
|
||||||
|
|
||||||
### General
|
|
||||||
|
|
||||||
Product Mixer services like Home Mixer are structured around Pipelines that split the execution
|
|
||||||
into transparent and structured steps.
|
|
||||||
|
|
||||||
Requests first go to Product Pipelines, which are used to select which Mixer Pipeline or
|
|
||||||
Recommendation Pipeline to run for a given request. Each Mixer or Recommendation
|
|
||||||
Pipeline may run multiple Candidate Pipelines to fetch candidates to include in the response.
|
|
||||||
|
|
||||||
Mixer Pipelines combine the results of multiple heterogeneous Candidate Pipelines together
|
|
||||||
(e.g. ads, tweets, users) while Recommendation Pipelines are used to score (via Scoring Pipelines)
|
|
||||||
and rank the results of homogenous Candidate Pipelines so that the top ranked ones can be returned.
|
|
||||||
These pipelines also marshall candidates into a domain object and then into a transport object
|
|
||||||
to return to the caller.
|
|
||||||
|
|
||||||
Candidate Pipelines fetch candidates from underlying Candidate Sources and perform some basic
|
|
||||||
operations on the Candidates, such as filtering out unwanted candidates, applying decorations,
|
|
||||||
and hydrating features.
|
|
||||||
|
|
||||||
The sections below describe the high level pipeline structure (non-exhaustive) for the main Home
|
|
||||||
Timeline tabs powered by Home Mixer.
|
|
||||||
|
|
||||||
### For You
|
|
||||||
|
|
||||||
- ForYouProductPipelineConfig
|
|
||||||
- ForYouScoredTweetsMixerPipelineConfig (main orchestration layer - mixes Tweets with ads and users)
|
|
||||||
- ForYouScoredTweetsCandidatePipelineConfig (fetch Tweets)
|
|
||||||
- ScoredTweetsRecommendationPipelineConfig (main Tweet recommendation layer)
|
|
||||||
- Fetch Tweet Candidates
|
|
||||||
- ScoredTweetsInNetworkCandidatePipelineConfig
|
|
||||||
- ScoredTweetsTweetMixerCandidatePipelineConfig
|
|
||||||
- ScoredTweetsUtegCandidatePipelineConfig
|
|
||||||
- ScoredTweetsFrsCandidatePipelineConfig
|
|
||||||
- Feature Hydration and Scoring
|
|
||||||
- ScoredTweetsScoringPipelineConfig
|
|
||||||
- ForYouConversationServiceCandidatePipelineConfig (backup reverse chron pipeline in case Scored Tweets fails)
|
|
||||||
- ForYouAdsCandidatePipelineConfig (fetch ads)
|
|
||||||
- ForYouWhoToFollowCandidatePipelineConfig (fetch users to recommend)
|
|
||||||
|
|
||||||
### Following
|
|
||||||
|
|
||||||
- FollowingProductPipelineConfig
|
|
||||||
- FollowingMixerPipelineConfig
|
|
||||||
- FollowingEarlybirdCandidatePipelineConfig (fetch tweets from Search Index)
|
|
||||||
- ConversationServiceCandidatePipelineConfig (fetch ancestors for conversation modules)
|
|
||||||
- FollowingAdsCandidatePipelineConfig (fetch ads)
|
|
||||||
- FollowingWhoToFollowCandidatePipelineConfig (fetch users to recommend)
|
|
||||||
|
|
||||||
### Lists
|
|
||||||
|
|
||||||
- ListTweetsProductPipelineConfig
|
|
||||||
- ListTweetsMixerPipelineConfig
|
|
||||||
- ListTweetsTimelineServiceCandidatePipelineConfig (fetch tweets from timeline service)
|
|
||||||
- ConversationServiceCandidatePipelineConfig (fetch ancestors for conversation modules)
|
|
||||||
- ListTweetsAdsCandidatePipelineConfig (fetch ads)
|
|
@ -1,51 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/com/google/inject:guice",
|
|
||||||
"3rdparty/jvm/javax/inject:javax.inject",
|
|
||||||
"3rdparty/jvm/net/codingwell:scala-guice",
|
|
||||||
"3rdparty/jvm/org/slf4j:slf4j-api",
|
|
||||||
"finagle/finagle-core/src/main",
|
|
||||||
"finagle/finagle-http/src/main/scala",
|
|
||||||
"finagle/finagle-thriftmux/src/main/scala",
|
|
||||||
"finatra-internal/mtls-http/src/main/scala",
|
|
||||||
"finatra-internal/mtls-thriftmux/src/main/scala",
|
|
||||||
"finatra/http-core/src/main/java/com/twitter/finatra/http",
|
|
||||||
"finatra/inject/inject-app/src/main/java/com/twitter/inject/annotations",
|
|
||||||
"finatra/inject/inject-app/src/main/scala",
|
|
||||||
"finatra/inject/inject-core/src/main/scala",
|
|
||||||
"finatra/inject/inject-server/src/main/scala",
|
|
||||||
"finatra/inject/inject-utils/src/main/scala",
|
|
||||||
"home-mixer/server/src/main/resources",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/controller",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/federated",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/module",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/param",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/product",
|
|
||||||
"home-mixer/thrift/src/main/thrift:thrift-scala",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/module",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/controllers",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module/stringcenter",
|
|
||||||
"product-mixer/core/src/main/thrift/com/twitter/product_mixer/core:thrift-scala",
|
|
||||||
"src/thrift/com/twitter/timelines/render:thrift-scala",
|
|
||||||
"strato/config/columns/auth-context:auth-context-strato-client",
|
|
||||||
"strato/config/columns/gizmoduck:gizmoduck-strato-client",
|
|
||||||
"strato/src/main/scala/com/twitter/strato/fed",
|
|
||||||
"strato/src/main/scala/com/twitter/strato/fed/server",
|
|
||||||
"stringcenter/client",
|
|
||||||
"stringcenter/client/src/main/java",
|
|
||||||
"stringcenter/client/src/main/scala/com/twitter/stringcenter/client",
|
|
||||||
"thrift-web-forms/src/main/scala/com/twitter/thriftwebforms/view",
|
|
||||||
"timelines/src/main/scala/com/twitter/timelines/config",
|
|
||||||
"timelines/src/main/scala/com/twitter/timelines/features/app",
|
|
||||||
"twitter-server-internal",
|
|
||||||
"twitter-server/server/src/main/scala",
|
|
||||||
"util/util-app/src/main/scala",
|
|
||||||
"util/util-core:scala",
|
|
||||||
"util/util-slf4j-api/src/main/scala",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,18 +0,0 @@
|
|||||||
package com.twitter.home_mixer
|
|
||||||
|
|
||||||
import com.twitter.finatra.http.routing.HttpWarmup
|
|
||||||
import com.twitter.finatra.httpclient.RequestBuilder._
|
|
||||||
import com.twitter.util.logging.Logging
|
|
||||||
import com.twitter.inject.utils.Handler
|
|
||||||
import com.twitter.util.Try
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class HomeMixerHttpServerWarmupHandler @Inject() (warmup: HttpWarmup) extends Handler with Logging {
|
|
||||||
|
|
||||||
override def handle(): Unit = {
|
|
||||||
Try(warmup.send(get("/admin/product-mixer/product-pipelines"), admin = true)())
|
|
||||||
.onFailure(e => error(e.getMessage, e))
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,128 +0,0 @@
|
|||||||
package com.twitter.home_mixer
|
|
||||||
|
|
||||||
import com.google.inject.Module
|
|
||||||
import com.twitter.finagle.Filter
|
|
||||||
import com.twitter.finatra.annotations.DarkTrafficFilterType
|
|
||||||
import com.twitter.finatra.http.HttpServer
|
|
||||||
import com.twitter.finatra.http.routing.HttpRouter
|
|
||||||
import com.twitter.finatra.mtls.http.{Mtls => HttpMtls}
|
|
||||||
import com.twitter.finatra.mtls.thriftmux.Mtls
|
|
||||||
import com.twitter.finatra.mtls.thriftmux.modules.MtlsThriftWebFormsModule
|
|
||||||
import com.twitter.finatra.thrift.ThriftServer
|
|
||||||
import com.twitter.finatra.thrift.filters._
|
|
||||||
import com.twitter.finatra.thrift.routing.ThriftRouter
|
|
||||||
import com.twitter.home_mixer.controller.HomeThriftController
|
|
||||||
import com.twitter.home_mixer.federated.HomeMixerColumn
|
|
||||||
import com.twitter.home_mixer.module._
|
|
||||||
import com.twitter.home_mixer.param.GlobalParamConfigModule
|
|
||||||
import com.twitter.home_mixer.product.HomeMixerProductModule
|
|
||||||
import com.twitter.home_mixer.{thriftscala => st}
|
|
||||||
import com.twitter.product_mixer.component_library.module.AccountRecommendationsMixerModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.DarkTrafficFilterModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.EarlybirdModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.ExploreRankerClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.GizmoduckClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.OnboardingTaskServiceModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.SocialGraphServiceModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.TimelineRankerClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.TimelineScorerClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.TimelineServiceClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.TweetImpressionStoreModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.TweetMixerClientModule
|
|
||||||
import com.twitter.product_mixer.component_library.module.UserSessionStoreModule
|
|
||||||
import com.twitter.product_mixer.core.controllers.ProductMixerController
|
|
||||||
import com.twitter.product_mixer.core.module.LoggingThrowableExceptionMapper
|
|
||||||
import com.twitter.product_mixer.core.module.ProductMixerModule
|
|
||||||
import com.twitter.product_mixer.core.module.stringcenter.ProductScopeStringCenterModule
|
|
||||||
import com.twitter.strato.fed.StratoFed
|
|
||||||
import com.twitter.strato.fed.server.StratoFedServer
|
|
||||||
|
|
||||||
object HomeMixerServerMain extends HomeMixerServer
|
|
||||||
|
|
||||||
class HomeMixerServer
|
|
||||||
extends StratoFedServer
|
|
||||||
with ThriftServer
|
|
||||||
with Mtls
|
|
||||||
with HttpServer
|
|
||||||
with HttpMtls {
|
|
||||||
override val name = "home-mixer-server"
|
|
||||||
|
|
||||||
override val modules: Seq[Module] = Seq(
|
|
||||||
AccountRecommendationsMixerModule,
|
|
||||||
AdvertiserBrandSafetySettingsStoreModule,
|
|
||||||
BlenderClientModule,
|
|
||||||
ClientSentImpressionsPublisherModule,
|
|
||||||
ConversationServiceModule,
|
|
||||||
EarlybirdModule,
|
|
||||||
ExploreRankerClientModule,
|
|
||||||
FeedbackHistoryClientModule,
|
|
||||||
GizmoduckClientModule,
|
|
||||||
GlobalParamConfigModule,
|
|
||||||
HomeAdsCandidateSourceModule,
|
|
||||||
HomeMixerFlagsModule,
|
|
||||||
HomeMixerProductModule,
|
|
||||||
HomeMixerResourcesModule,
|
|
||||||
ImpressionBloomFilterModule,
|
|
||||||
InjectionHistoryClientModule,
|
|
||||||
ManhattanClientsModule,
|
|
||||||
ManhattanFeatureRepositoryModule,
|
|
||||||
ManhattanTweetImpressionStoreModule,
|
|
||||||
MemcachedFeatureRepositoryModule,
|
|
||||||
NaviModelClientModule,
|
|
||||||
OnboardingTaskServiceModule,
|
|
||||||
OptimizedStratoClientModule,
|
|
||||||
PeopleDiscoveryServiceModule,
|
|
||||||
ProductMixerModule,
|
|
||||||
RealGraphInNetworkScoresModule,
|
|
||||||
RealtimeAggregateFeatureRepositoryModule,
|
|
||||||
ScoredTweetsMemcacheModule,
|
|
||||||
ScribeEventPublisherModule,
|
|
||||||
SimClustersRecentEngagementsClientModule,
|
|
||||||
SocialGraphServiceModule,
|
|
||||||
StaleTweetsCacheModule,
|
|
||||||
ThriftFeatureRepositoryModule,
|
|
||||||
TimelineRankerClientModule,
|
|
||||||
TimelineScorerClientModule,
|
|
||||||
TimelineServiceClientModule,
|
|
||||||
TimelinesPersistenceStoreClientModule,
|
|
||||||
TopicSocialProofClientModule,
|
|
||||||
TweetImpressionStoreModule,
|
|
||||||
TweetMixerClientModule,
|
|
||||||
TweetypieClientModule,
|
|
||||||
TweetypieStaticEntitiesCacheClientModule,
|
|
||||||
UserSessionStoreModule,
|
|
||||||
new DarkTrafficFilterModule[st.HomeMixer.ReqRepServicePerEndpoint](),
|
|
||||||
new MtlsThriftWebFormsModule[st.HomeMixer.MethodPerEndpoint](this),
|
|
||||||
new ProductScopeStringCenterModule()
|
|
||||||
)
|
|
||||||
|
|
||||||
override def configureThrift(router: ThriftRouter): Unit = {
|
|
||||||
router
|
|
||||||
.filter[LoggingMDCFilter]
|
|
||||||
.filter[TraceIdMDCFilter]
|
|
||||||
.filter[ThriftMDCFilter]
|
|
||||||
.filter[StatsFilter]
|
|
||||||
.filter[AccessLoggingFilter]
|
|
||||||
.filter[ExceptionMappingFilter]
|
|
||||||
.filter[Filter.TypeAgnostic, DarkTrafficFilterType]
|
|
||||||
.exceptionMapper[LoggingThrowableExceptionMapper]
|
|
||||||
.exceptionMapper[PipelineFailureExceptionMapper]
|
|
||||||
.add[HomeThriftController]
|
|
||||||
}
|
|
||||||
|
|
||||||
override def configureHttp(router: HttpRouter): Unit =
|
|
||||||
router.add(
|
|
||||||
ProductMixerController[st.HomeMixer.MethodPerEndpoint](
|
|
||||||
this.injector,
|
|
||||||
st.HomeMixer.ExecutePipeline))
|
|
||||||
|
|
||||||
override val dest: String = "/s/home-mixer/home-mixer:strato"
|
|
||||||
|
|
||||||
override val columns: Seq[Class[_ <: StratoFed.Column]] =
|
|
||||||
Seq(classOf[HomeMixerColumn])
|
|
||||||
|
|
||||||
override protected def warmup(): Unit = {
|
|
||||||
handle[HomeMixerThriftServerWarmupHandler]()
|
|
||||||
handle[HomeMixerHttpServerWarmupHandler]()
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,73 +0,0 @@
|
|||||||
package com.twitter.home_mixer
|
|
||||||
|
|
||||||
import com.twitter.finagle.thrift.ClientId
|
|
||||||
import com.twitter.finatra.thrift.routing.ThriftWarmup
|
|
||||||
import com.twitter.home_mixer.{thriftscala => st}
|
|
||||||
import com.twitter.util.logging.Logging
|
|
||||||
import com.twitter.inject.utils.Handler
|
|
||||||
import com.twitter.product_mixer.core.{thriftscala => pt}
|
|
||||||
import com.twitter.scrooge.Request
|
|
||||||
import com.twitter.scrooge.Response
|
|
||||||
import com.twitter.util.Return
|
|
||||||
import com.twitter.util.Throw
|
|
||||||
import com.twitter.util.Try
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class HomeMixerThriftServerWarmupHandler @Inject() (warmup: ThriftWarmup)
|
|
||||||
extends Handler
|
|
||||||
with Logging {
|
|
||||||
|
|
||||||
private val clientId = ClientId("thrift-warmup-client")
|
|
||||||
|
|
||||||
def handle(): Unit = {
|
|
||||||
val testIds = Seq(1, 2, 3)
|
|
||||||
try {
|
|
||||||
clientId.asCurrent {
|
|
||||||
testIds.foreach { id =>
|
|
||||||
val warmupReq = warmupQuery(id)
|
|
||||||
info(s"Sending warm-up request to service with query: $warmupReq")
|
|
||||||
warmup.sendRequest(
|
|
||||||
method = st.HomeMixer.GetUrtResponse,
|
|
||||||
req = Request(st.HomeMixer.GetUrtResponse.Args(warmupReq)))(assertWarmupResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case e: Throwable => error(e.getMessage, e)
|
|
||||||
}
|
|
||||||
info("Warm-up done.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def warmupQuery(userId: Long): st.HomeMixerRequest = {
|
|
||||||
val clientContext = pt.ClientContext(
|
|
||||||
userId = Some(userId),
|
|
||||||
guestId = None,
|
|
||||||
appId = Some(12345L),
|
|
||||||
ipAddress = Some("0.0.0.0"),
|
|
||||||
userAgent = Some("FAKE_USER_AGENT_FOR_WARMUPS"),
|
|
||||||
countryCode = Some("US"),
|
|
||||||
languageCode = Some("en"),
|
|
||||||
isTwoffice = None,
|
|
||||||
userRoles = None,
|
|
||||||
deviceId = Some("FAKE_DEVICE_ID_FOR_WARMUPS")
|
|
||||||
)
|
|
||||||
st.HomeMixerRequest(
|
|
||||||
clientContext = clientContext,
|
|
||||||
product = st.Product.Following,
|
|
||||||
productContext = Some(st.ProductContext.Following(st.Following())),
|
|
||||||
maxResults = Some(3)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def assertWarmupResponse(
|
|
||||||
result: Try[Response[st.HomeMixer.GetUrtResponse.SuccessType]]
|
|
||||||
): Unit = {
|
|
||||||
result match {
|
|
||||||
case Return(_) => // ok
|
|
||||||
case Throw(exception) =>
|
|
||||||
warn("Error performing warm-up request.")
|
|
||||||
error(exception.getMessage, exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/candidate_source",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/decorator/urt/builder",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/feature_hydrator",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/filter",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/gate",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/query_transformer",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/model",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/model/request",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/service",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/candidate_source/tweetconvosvc",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/decorator/urt",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/filter",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/gate",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/transformer",
|
|
||||||
],
|
|
||||||
exports = [
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/candidate_source/tweetconvosvc",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,116 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.InNetworkFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.NamesFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.TweetypieFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.filter.InvalidConversationModuleFilter
|
|
||||||
import com.twitter.home_mixer.functional_component.filter.InvalidSubscriptionTweetFilter
|
|
||||||
import com.twitter.home_mixer.functional_component.filter.RetweetDeduplicationFilter
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.AuthorIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.InReplyToTweetIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.IsHydratedFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.QuotedTweetDroppedFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.SourceTweetIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.SourceUserIdFeature
|
|
||||||
import com.twitter.home_mixer.service.HomeMixerAlertConfig
|
|
||||||
import com.twitter.product_mixer.component_library.candidate_source.tweetconvosvc.ConversationServiceCandidateSource
|
|
||||||
import com.twitter.product_mixer.component_library.candidate_source.tweetconvosvc.ConversationServiceCandidateSourceRequest
|
|
||||||
import com.twitter.product_mixer.component_library.candidate_source.tweetconvosvc.TweetWithConversationMetadata
|
|
||||||
import com.twitter.product_mixer.component_library.filter.FeatureFilter
|
|
||||||
import com.twitter.product_mixer.component_library.filter.PredicateFeatureFilter
|
|
||||||
import com.twitter.product_mixer.component_library.model.candidate.TweetCandidate
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.BaseCandidateSource
|
|
||||||
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseCandidateFeatureHydrator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.filter.Filter
|
|
||||||
import com.twitter.product_mixer.core.functional_component.gate.BaseGate
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidateFeatureTransformer
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.DependentCandidatePipelineQueryTransformer
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.FilterIdentifier
|
|
||||||
import com.twitter.product_mixer.core.pipeline.PipelineQuery
|
|
||||||
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Candidate Pipeline Config that fetches tweets from the Conversation Service Candidate Source
|
|
||||||
*/
|
|
||||||
class ConversationServiceCandidatePipelineConfig[Query <: PipelineQuery](
|
|
||||||
conversationServiceCandidateSource: ConversationServiceCandidateSource,
|
|
||||||
tweetypieFeatureHydrator: TweetypieFeatureHydrator,
|
|
||||||
namesFeatureHydrator: NamesFeatureHydrator,
|
|
||||||
invalidSubscriptionTweetFilter: InvalidSubscriptionTweetFilter,
|
|
||||||
override val gates: Seq[BaseGate[Query]],
|
|
||||||
override val decorator: Option[CandidateDecorator[Query, TweetCandidate]])
|
|
||||||
extends DependentCandidatePipelineConfig[
|
|
||||||
Query,
|
|
||||||
ConversationServiceCandidateSourceRequest,
|
|
||||||
TweetWithConversationMetadata,
|
|
||||||
TweetCandidate
|
|
||||||
] {
|
|
||||||
|
|
||||||
override val identifier: CandidatePipelineIdentifier =
|
|
||||||
CandidatePipelineIdentifier("ConversationService")
|
|
||||||
|
|
||||||
private val TweetypieHydratedFilterId = "TweetypieHydrated"
|
|
||||||
private val QuotedTweetDroppedFilterId = "QuotedTweetDropped"
|
|
||||||
|
|
||||||
override val candidateSource: BaseCandidateSource[
|
|
||||||
ConversationServiceCandidateSourceRequest,
|
|
||||||
TweetWithConversationMetadata
|
|
||||||
] = conversationServiceCandidateSource
|
|
||||||
|
|
||||||
override val queryTransformer: DependentCandidatePipelineQueryTransformer[
|
|
||||||
Query,
|
|
||||||
ConversationServiceCandidateSourceRequest
|
|
||||||
] = { (_, candidates) =>
|
|
||||||
val tweetsWithConversationMetadata = candidates.map { candidate =>
|
|
||||||
TweetWithConversationMetadata(
|
|
||||||
tweetId = candidate.candidateIdLong,
|
|
||||||
userId = candidate.features.getOrElse(AuthorIdFeature, None),
|
|
||||||
sourceTweetId = candidate.features.getOrElse(SourceTweetIdFeature, None),
|
|
||||||
sourceUserId = candidate.features.getOrElse(SourceUserIdFeature, None),
|
|
||||||
inReplyToTweetId = candidate.features.getOrElse(InReplyToTweetIdFeature, None),
|
|
||||||
conversationId = None,
|
|
||||||
ancestors = Seq.empty
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ConversationServiceCandidateSourceRequest(tweetsWithConversationMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val featuresFromCandidateSourceTransformers: Seq[
|
|
||||||
CandidateFeatureTransformer[TweetWithConversationMetadata]
|
|
||||||
] = Seq(ConversationServiceResponseFeatureTransformer)
|
|
||||||
|
|
||||||
override val resultTransformer: CandidatePipelineResultsTransformer[
|
|
||||||
TweetWithConversationMetadata,
|
|
||||||
TweetCandidate
|
|
||||||
] = { sourceResult => TweetCandidate(id = sourceResult.tweetId) }
|
|
||||||
|
|
||||||
override val preFilterFeatureHydrationPhase1: Seq[
|
|
||||||
BaseCandidateFeatureHydrator[Query, TweetCandidate, _]
|
|
||||||
] = Seq(
|
|
||||||
tweetypieFeatureHydrator,
|
|
||||||
InNetworkFeatureHydrator,
|
|
||||||
)
|
|
||||||
|
|
||||||
override def filters: Seq[Filter[Query, TweetCandidate]] = Seq(
|
|
||||||
RetweetDeduplicationFilter,
|
|
||||||
FeatureFilter.fromFeature(FilterIdentifier(TweetypieHydratedFilterId), IsHydratedFeature),
|
|
||||||
PredicateFeatureFilter.fromPredicate(
|
|
||||||
FilterIdentifier(QuotedTweetDroppedFilterId),
|
|
||||||
shouldKeepCandidate = { features => !features.getOrElse(QuotedTweetDroppedFeature, false) }
|
|
||||||
),
|
|
||||||
invalidSubscriptionTweetFilter,
|
|
||||||
InvalidConversationModuleFilter
|
|
||||||
)
|
|
||||||
|
|
||||||
override val postFilterFeatureHydration: Seq[
|
|
||||||
BaseCandidateFeatureHydrator[Query, TweetCandidate, _]
|
|
||||||
] = Seq(namesFeatureHydrator)
|
|
||||||
|
|
||||||
override val alerts = Seq(
|
|
||||||
HomeMixerAlertConfig.BusinessHours.defaultSuccessRateAlert(),
|
|
||||||
HomeMixerAlertConfig.BusinessHours.defaultEmptyResponseRateAlert()
|
|
||||||
)
|
|
||||||
}
|
|
Binary file not shown.
@ -1,34 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.NamesFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.TweetypieFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.filter.InvalidSubscriptionTweetFilter
|
|
||||||
import com.twitter.product_mixer.component_library.candidate_source.tweetconvosvc.ConversationServiceCandidateSource
|
|
||||||
import com.twitter.product_mixer.component_library.model.candidate.TweetCandidate
|
|
||||||
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.gate.BaseGate
|
|
||||||
import com.twitter.product_mixer.core.pipeline.PipelineQuery
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ConversationServiceCandidatePipelineConfigBuilder[Query <: PipelineQuery] @Inject() (
|
|
||||||
conversationServiceCandidateSource: ConversationServiceCandidateSource,
|
|
||||||
tweetypieFeatureHydrator: TweetypieFeatureHydrator,
|
|
||||||
invalidSubscriptionTweetFilter: InvalidSubscriptionTweetFilter,
|
|
||||||
namesFeatureHydrator: NamesFeatureHydrator) {
|
|
||||||
|
|
||||||
def build(
|
|
||||||
gates: Seq[BaseGate[Query]] = Seq.empty,
|
|
||||||
decorator: Option[CandidateDecorator[Query, TweetCandidate]] = None
|
|
||||||
): ConversationServiceCandidatePipelineConfig[Query] = {
|
|
||||||
new ConversationServiceCandidatePipelineConfig(
|
|
||||||
conversationServiceCandidateSource,
|
|
||||||
tweetypieFeatureHydrator,
|
|
||||||
namesFeatureHydrator,
|
|
||||||
invalidSubscriptionTweetFilter,
|
|
||||||
gates,
|
|
||||||
decorator
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,39 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures._
|
|
||||||
import com.twitter.product_mixer.component_library.candidate_source.tweetconvosvc.TweetWithConversationMetadata
|
|
||||||
import com.twitter.product_mixer.core.feature.Feature
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMapBuilder
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidateFeatureTransformer
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.TransformerIdentifier
|
|
||||||
import com.twitter.timelineservice.suggests.thriftscala.SuggestType
|
|
||||||
|
|
||||||
object ConversationServiceResponseFeatureTransformer
|
|
||||||
extends CandidateFeatureTransformer[TweetWithConversationMetadata] {
|
|
||||||
|
|
||||||
override val identifier: TransformerIdentifier =
|
|
||||||
TransformerIdentifier("ConversationServiceResponse")
|
|
||||||
|
|
||||||
override val features: Set[Feature[_, _]] = Set(
|
|
||||||
AuthorIdFeature,
|
|
||||||
InReplyToTweetIdFeature,
|
|
||||||
IsRetweetFeature,
|
|
||||||
SourceTweetIdFeature,
|
|
||||||
SourceUserIdFeature,
|
|
||||||
ConversationModuleFocalTweetIdFeature,
|
|
||||||
AncestorsFeature,
|
|
||||||
SuggestTypeFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
override def transform(candidate: TweetWithConversationMetadata): FeatureMap = FeatureMapBuilder()
|
|
||||||
.add(AuthorIdFeature, candidate.userId)
|
|
||||||
.add(InReplyToTweetIdFeature, candidate.inReplyToTweetId)
|
|
||||||
.add(IsRetweetFeature, candidate.sourceTweetId.isDefined)
|
|
||||||
.add(SourceTweetIdFeature, candidate.sourceTweetId)
|
|
||||||
.add(SourceUserIdFeature, candidate.sourceUserId)
|
|
||||||
.add(ConversationModuleFocalTweetIdFeature, candidate.conversationId)
|
|
||||||
.add(AncestorsFeature, candidate.ancestors)
|
|
||||||
.add(SuggestTypeFeature, Some(SuggestType.RankedOrganicTweet))
|
|
||||||
.build()
|
|
||||||
}
|
|
Binary file not shown.
@ -1,84 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.functional_component.candidate_source.StaleTweetsCacheCandidateSource
|
|
||||||
import com.twitter.home_mixer.functional_component.decorator.urt.builder.HomeFeedbackActionInfoBuilder
|
|
||||||
import com.twitter.home_mixer.functional_component.feature_hydrator.NamesFeatureHydrator
|
|
||||||
import com.twitter.home_mixer.functional_component.query_transformer.EditedTweetsCandidatePipelineQueryTransformer
|
|
||||||
import com.twitter.home_mixer.service.HomeMixerAlertConfig
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.UrtItemCandidateDecorator
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.contextual_ref.ContextualTweetRefBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.tweet.TweetCandidateUrtItemBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.metadata.EmptyClientEventInfoBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.model.candidate.TweetCandidate
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.BaseCandidateSource
|
|
||||||
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.feature_hydrator.BaseCandidateFeatureHydrator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineQueryTransformer
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.rtf.safety_level.TimelineFocalTweetSafetyLevel
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.contextual_ref.TweetHydrationContext
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.item.tweet.TweetItem
|
|
||||||
import com.twitter.product_mixer.core.pipeline.PipelineQuery
|
|
||||||
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Candidate Pipeline Config that fetches edited tweets from the Stale Tweets Cache
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
case class EditedTweetsCandidatePipelineConfig @Inject() (
|
|
||||||
staleTweetsCacheCandidateSource: StaleTweetsCacheCandidateSource,
|
|
||||||
namesFeatureHydrator: NamesFeatureHydrator,
|
|
||||||
homeFeedbackActionInfoBuilder: HomeFeedbackActionInfoBuilder)
|
|
||||||
extends DependentCandidatePipelineConfig[
|
|
||||||
PipelineQuery,
|
|
||||||
Seq[Long],
|
|
||||||
Long,
|
|
||||||
TweetCandidate
|
|
||||||
] {
|
|
||||||
|
|
||||||
override val identifier: CandidatePipelineIdentifier = CandidatePipelineIdentifier("EditedTweets")
|
|
||||||
|
|
||||||
override val candidateSource: BaseCandidateSource[Seq[Long], Long] =
|
|
||||||
staleTweetsCacheCandidateSource
|
|
||||||
|
|
||||||
override val queryTransformer: CandidatePipelineQueryTransformer[
|
|
||||||
PipelineQuery,
|
|
||||||
Seq[Long]
|
|
||||||
] = EditedTweetsCandidatePipelineQueryTransformer
|
|
||||||
|
|
||||||
override val resultTransformer: CandidatePipelineResultsTransformer[
|
|
||||||
Long,
|
|
||||||
TweetCandidate
|
|
||||||
] = { candidate => TweetCandidate(id = candidate) }
|
|
||||||
|
|
||||||
override val postFilterFeatureHydration: Seq[
|
|
||||||
BaseCandidateFeatureHydrator[PipelineQuery, TweetCandidate, _]
|
|
||||||
] = Seq(namesFeatureHydrator)
|
|
||||||
|
|
||||||
override val decorator: Option[CandidateDecorator[PipelineQuery, TweetCandidate]] = {
|
|
||||||
val tweetItemBuilder = TweetCandidateUrtItemBuilder[PipelineQuery, TweetCandidate](
|
|
||||||
clientEventInfoBuilder = EmptyClientEventInfoBuilder,
|
|
||||||
entryIdToReplaceBuilder = Some((_, candidate, _) =>
|
|
||||||
Some(s"${TweetItem.TweetEntryNamespace}-${candidate.id.toString}")),
|
|
||||||
contextualTweetRefBuilder = Some(
|
|
||||||
ContextualTweetRefBuilder(
|
|
||||||
TweetHydrationContext(
|
|
||||||
// Apply safety level that includes canonical VF treatments that apply regardless of context.
|
|
||||||
safetyLevelOverride = Some(TimelineFocalTweetSafetyLevel),
|
|
||||||
outerTweetContext = None
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
feedbackActionInfoBuilder = Some(homeFeedbackActionInfoBuilder)
|
|
||||||
)
|
|
||||||
|
|
||||||
Some(UrtItemCandidateDecorator(tweetItemBuilder))
|
|
||||||
}
|
|
||||||
|
|
||||||
override val alerts = Seq(
|
|
||||||
HomeMixerAlertConfig.BusinessHours.defaultSuccessRateAlert(99.5, 50, 60, 60)
|
|
||||||
)
|
|
||||||
}
|
|
Binary file not shown.
@ -1,123 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.conversions.DurationOps._
|
|
||||||
import com.twitter.home_mixer.functional_component.gate.RequestContextNotGate
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.GetNewerFeature
|
|
||||||
import com.twitter.home_mixer.model.request.DeviceContext
|
|
||||||
import com.twitter.home_mixer.model.request.HasDeviceContext
|
|
||||||
import com.twitter.home_mixer.service.HomeMixerAlertConfig
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.UrtItemCandidateDecorator
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.alert.DurationParamBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.alert.ShowAlertCandidateUrtItemBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.alert.StaticShowAlertColorConfigurationBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.alert.StaticShowAlertDisplayLocationBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.alert.StaticShowAlertIconDisplayInfoBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.gate.FeatureGate
|
|
||||||
import com.twitter.product_mixer.component_library.model.candidate.ShowAlertCandidate
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.StaticCandidateSource
|
|
||||||
import com.twitter.product_mixer.core.functional_component.configapi.StaticParam
|
|
||||||
import com.twitter.product_mixer.core.functional_component.decorator.CandidateDecorator
|
|
||||||
import com.twitter.product_mixer.core.functional_component.decorator.urt.builder.item.alert.BaseDurationBuilder
|
|
||||||
import com.twitter.product_mixer.core.functional_component.gate.Gate
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineQueryTransformer
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidatePipelineResultsTransformer
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidatePipelineIdentifier
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.alert.NewTweets
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.alert.ShowAlertColorConfiguration
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.alert.ShowAlertIconDisplayInfo
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.alert.Top
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.alert.UpArrow
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.color.TwitterBlueRosettaColor
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.color.WhiteRosettaColor
|
|
||||||
import com.twitter.product_mixer.core.pipeline.PipelineQuery
|
|
||||||
import com.twitter.product_mixer.core.pipeline.candidate.DependentCandidatePipelineConfig
|
|
||||||
import com.twitter.util.Duration
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Candidate Pipeline Config that creates the New Tweets Pill
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
class NewTweetsPillCandidatePipelineConfig[Query <: PipelineQuery with HasDeviceContext] @Inject() (
|
|
||||||
) extends DependentCandidatePipelineConfig[
|
|
||||||
Query,
|
|
||||||
Unit,
|
|
||||||
ShowAlertCandidate,
|
|
||||||
ShowAlertCandidate
|
|
||||||
] {
|
|
||||||
import NewTweetsPillCandidatePipelineConfig._
|
|
||||||
|
|
||||||
override val identifier: CandidatePipelineIdentifier =
|
|
||||||
CandidatePipelineIdentifier("NewTweetsPill")
|
|
||||||
|
|
||||||
override val gates: Seq[Gate[Query]] = Seq(
|
|
||||||
RequestContextNotGate(Seq(DeviceContext.RequestContext.PullToRefresh)),
|
|
||||||
FeatureGate.fromFeature(GetNewerFeature)
|
|
||||||
)
|
|
||||||
|
|
||||||
override val candidateSource: CandidateSource[Unit, ShowAlertCandidate] =
|
|
||||||
StaticCandidateSource(
|
|
||||||
CandidateSourceIdentifier(identifier.name),
|
|
||||||
Seq(ShowAlertCandidate(id = identifier.name, userIds = Seq.empty))
|
|
||||||
)
|
|
||||||
|
|
||||||
override val queryTransformer: CandidatePipelineQueryTransformer[Query, Unit] = { _ => Unit }
|
|
||||||
|
|
||||||
override val resultTransformer: CandidatePipelineResultsTransformer[
|
|
||||||
ShowAlertCandidate,
|
|
||||||
ShowAlertCandidate
|
|
||||||
] = { candidate => candidate }
|
|
||||||
|
|
||||||
override val decorator: Option[CandidateDecorator[Query, ShowAlertCandidate]] = {
|
|
||||||
val triggerDelayBuilder = new BaseDurationBuilder[Query] {
|
|
||||||
override def apply(
|
|
||||||
query: Query,
|
|
||||||
candidate: ShowAlertCandidate,
|
|
||||||
features: FeatureMap
|
|
||||||
): Option[Duration] = {
|
|
||||||
val delay = query.deviceContext.flatMap(_.requestContextValue) match {
|
|
||||||
case Some(DeviceContext.RequestContext.TweetSelfThread) => 0.millis
|
|
||||||
case Some(DeviceContext.RequestContext.ManualRefresh) => 0.millis
|
|
||||||
case _ => TriggerDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val homeShowAlertCandidateBuilder = ShowAlertCandidateUrtItemBuilder(
|
|
||||||
alertType = NewTweets,
|
|
||||||
colorConfigBuilder = StaticShowAlertColorConfigurationBuilder(DefaultColorConfig),
|
|
||||||
displayLocationBuilder = StaticShowAlertDisplayLocationBuilder(Top),
|
|
||||||
triggerDelayBuilder = Some(triggerDelayBuilder),
|
|
||||||
displayDurationBuilder = Some(DurationParamBuilder(StaticParam(DisplayDuration))),
|
|
||||||
iconDisplayInfoBuilder = Some(StaticShowAlertIconDisplayInfoBuilder(DefaultIconDisplayInfo))
|
|
||||||
)
|
|
||||||
|
|
||||||
Some(UrtItemCandidateDecorator(homeShowAlertCandidateBuilder))
|
|
||||||
}
|
|
||||||
|
|
||||||
override val alerts = Seq(
|
|
||||||
HomeMixerAlertConfig.BusinessHours.defaultSuccessRateAlert(),
|
|
||||||
HomeMixerAlertConfig.BusinessHours.defaultEmptyResponseRateAlert()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
object NewTweetsPillCandidatePipelineConfig {
|
|
||||||
val DefaultColorConfig: ShowAlertColorConfiguration = ShowAlertColorConfiguration(
|
|
||||||
background = TwitterBlueRosettaColor,
|
|
||||||
text = WhiteRosettaColor,
|
|
||||||
border = Some(WhiteRosettaColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
val DefaultIconDisplayInfo: ShowAlertIconDisplayInfo =
|
|
||||||
ShowAlertIconDisplayInfo(icon = UpArrow, tint = WhiteRosettaColor)
|
|
||||||
|
|
||||||
// Unlimited display time (until user takes action)
|
|
||||||
val DisplayDuration = -1.millisecond
|
|
||||||
val TriggerDelay = 4.minutes
|
|
||||||
}
|
|
Binary file not shown.
@ -1,34 +0,0 @@
|
|||||||
package com.twitter.home_mixer.candidate_pipeline
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.AuthorIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.InReplyToTweetIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.IsRetweetFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.SourceTweetIdFeature
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.SourceUserIdFeature
|
|
||||||
import com.twitter.product_mixer.core.feature.Feature
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMapBuilder
|
|
||||||
import com.twitter.product_mixer.core.functional_component.transformer.CandidateFeatureTransformer
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.TransformerIdentifier
|
|
||||||
import com.twitter.timelineservice.{thriftscala => t}
|
|
||||||
|
|
||||||
object TimelineServiceResponseFeatureTransformer extends CandidateFeatureTransformer[t.Tweet] {
|
|
||||||
|
|
||||||
override val identifier: TransformerIdentifier = TransformerIdentifier("TimelineServiceResponse")
|
|
||||||
|
|
||||||
override val features: Set[Feature[_, _]] = Set(
|
|
||||||
AuthorIdFeature,
|
|
||||||
InReplyToTweetIdFeature,
|
|
||||||
IsRetweetFeature,
|
|
||||||
SourceTweetIdFeature,
|
|
||||||
SourceUserIdFeature,
|
|
||||||
)
|
|
||||||
|
|
||||||
override def transform(candidate: t.Tweet): FeatureMap = FeatureMapBuilder()
|
|
||||||
.add(AuthorIdFeature, candidate.userId)
|
|
||||||
.add(InReplyToTweetIdFeature, candidate.inReplyToStatusId)
|
|
||||||
.add(IsRetweetFeature, candidate.sourceStatusId.isDefined)
|
|
||||||
.add(SourceTweetIdFeature, candidate.sourceStatusId)
|
|
||||||
.add(SourceUserIdFeature, candidate.sourceUserId)
|
|
||||||
.build()
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"finatra/thrift/src/main/scala/com/twitter/finatra/thrift:controller",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/marshaller/request",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/service",
|
|
||||||
"home-mixer/thrift/src/main/thrift:thrift-scala",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/controllers",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/configapi",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/debug_query",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/urt",
|
|
||||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,51 +0,0 @@
|
|||||||
package com.twitter.home_mixer.controller
|
|
||||||
|
|
||||||
import com.twitter.finatra.thrift.Controller
|
|
||||||
import com.twitter.home_mixer.marshaller.request.HomeMixerRequestUnmarshaller
|
|
||||||
import com.twitter.home_mixer.model.request.HomeMixerRequest
|
|
||||||
import com.twitter.home_mixer.service.ScoredTweetsService
|
|
||||||
import com.twitter.home_mixer.{thriftscala => t}
|
|
||||||
import com.twitter.product_mixer.core.controllers.DebugTwitterContext
|
|
||||||
import com.twitter.product_mixer.core.functional_component.configapi.ParamsBuilder
|
|
||||||
import com.twitter.product_mixer.core.service.debug_query.DebugQueryService
|
|
||||||
import com.twitter.product_mixer.core.service.urt.UrtService
|
|
||||||
import com.twitter.snowflake.id.SnowflakeId
|
|
||||||
import com.twitter.stitch.Stitch
|
|
||||||
import com.twitter.timelines.configapi.Params
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class HomeThriftController @Inject() (
|
|
||||||
homeRequestUnmarshaller: HomeMixerRequestUnmarshaller,
|
|
||||||
urtService: UrtService,
|
|
||||||
scoredTweetsService: ScoredTweetsService,
|
|
||||||
paramsBuilder: ParamsBuilder)
|
|
||||||
extends Controller(t.HomeMixer)
|
|
||||||
with DebugTwitterContext {
|
|
||||||
|
|
||||||
handle(t.HomeMixer.GetUrtResponse) { args: t.HomeMixer.GetUrtResponse.Args =>
|
|
||||||
val request = homeRequestUnmarshaller(args.request)
|
|
||||||
val params = buildParams(request)
|
|
||||||
Stitch.run(urtService.getUrtResponse[HomeMixerRequest](request, params))
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(t.HomeMixer.GetScoredTweetsResponse) { args: t.HomeMixer.GetScoredTweetsResponse.Args =>
|
|
||||||
val request = homeRequestUnmarshaller(args.request)
|
|
||||||
val params = buildParams(request)
|
|
||||||
withDebugTwitterContext(request.clientContext) {
|
|
||||||
Stitch.run(scoredTweetsService.getScoredTweetsResponse[HomeMixerRequest](request, params))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def buildParams(request: HomeMixerRequest): Params = {
|
|
||||||
val userAgeOpt = request.clientContext.userId.map { userId =>
|
|
||||||
SnowflakeId.timeFromIdOpt(userId).map(_.untilNow.inDays).getOrElse(Int.MaxValue)
|
|
||||||
}
|
|
||||||
val fsCustomMapInput = userAgeOpt.map("account_age_in_days" -> _).toMap
|
|
||||||
paramsBuilder.build(
|
|
||||||
clientContext = request.clientContext,
|
|
||||||
product = request.product,
|
|
||||||
featureOverrides = request.debugParams.flatMap(_.featureOverrides).getOrElse(Map.empty),
|
|
||||||
fsCustomMapInput = fsCustomMapInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/marshaller/request",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/model/request",
|
|
||||||
"home-mixer/thrift/src/main/thrift:thrift-scala",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/configapi",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/product",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/registry",
|
|
||||||
"product-mixer/core/src/main/thrift/com/twitter/product_mixer/core:thrift-scala",
|
|
||||||
"src/thrift/com/twitter/gizmoduck:thrift-scala",
|
|
||||||
"src/thrift/com/twitter/timelines/render:thrift-scala",
|
|
||||||
"stitch/stitch-repo/src/main/scala",
|
|
||||||
"strato/config/columns/auth-context:auth-context-strato-client",
|
|
||||||
"strato/config/columns/gizmoduck:gizmoduck-strato-client",
|
|
||||||
"strato/config/src/thrift/com/twitter/strato/graphql/timelines:graphql-timelines-scala",
|
|
||||||
"strato/src/main/scala/com/twitter/strato/callcontext",
|
|
||||||
"strato/src/main/scala/com/twitter/strato/fed",
|
|
||||||
"strato/src/main/scala/com/twitter/strato/fed/server",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,217 +0,0 @@
|
|||||||
package com.twitter.home_mixer.federated
|
|
||||||
|
|
||||||
import com.twitter.gizmoduck.{thriftscala => gd}
|
|
||||||
import com.twitter.home_mixer.marshaller.request.HomeMixerRequestUnmarshaller
|
|
||||||
import com.twitter.home_mixer.model.request.HomeMixerRequest
|
|
||||||
import com.twitter.home_mixer.{thriftscala => hm}
|
|
||||||
import com.twitter.product_mixer.core.functional_component.configapi.ParamsBuilder
|
|
||||||
import com.twitter.product_mixer.core.pipeline.product.ProductPipelineRequest
|
|
||||||
import com.twitter.product_mixer.core.pipeline.product.ProductPipelineResult
|
|
||||||
import com.twitter.product_mixer.core.product.registry.ProductPipelineRegistry
|
|
||||||
import com.twitter.product_mixer.core.{thriftscala => pm}
|
|
||||||
import com.twitter.stitch.Arrow
|
|
||||||
import com.twitter.stitch.Stitch
|
|
||||||
import com.twitter.strato.callcontext.CallContext
|
|
||||||
import com.twitter.strato.catalog.OpMetadata
|
|
||||||
import com.twitter.strato.config._
|
|
||||||
import com.twitter.strato.data._
|
|
||||||
import com.twitter.strato.fed.StratoFed
|
|
||||||
import com.twitter.strato.generated.client.auth_context.AuditIpClientColumn
|
|
||||||
import com.twitter.strato.generated.client.gizmoduck.CompositeOnUserClientColumn
|
|
||||||
import com.twitter.strato.graphql.timelines.{thriftscala => gql}
|
|
||||||
import com.twitter.strato.thrift.ScroogeConv
|
|
||||||
import com.twitter.timelines.render.{thriftscala => tr}
|
|
||||||
import com.twitter.util.Try
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class HomeMixerColumn @Inject() (
|
|
||||||
homeMixerRequestUnmarshaller: HomeMixerRequestUnmarshaller,
|
|
||||||
compositeOnUserClientColumn: CompositeOnUserClientColumn,
|
|
||||||
auditIpClientColumn: AuditIpClientColumn,
|
|
||||||
paramsBuilder: ParamsBuilder,
|
|
||||||
productPipelineRegistry: ProductPipelineRegistry)
|
|
||||||
extends StratoFed.Column(HomeMixerColumn.Path)
|
|
||||||
with StratoFed.Fetch.Arrow {
|
|
||||||
|
|
||||||
override val contactInfo: ContactInfo = ContactInfo(
|
|
||||||
contactEmail = "",
|
|
||||||
ldapGroup = "",
|
|
||||||
slackRoomId = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
override val metadata: OpMetadata =
|
|
||||||
OpMetadata(
|
|
||||||
lifecycle = Some(Lifecycle.Production),
|
|
||||||
description =
|
|
||||||
Some(Description.PlainText("Federated Strato column for Timelines served via Home Mixer"))
|
|
||||||
)
|
|
||||||
|
|
||||||
private val bouncerAccess: Seq[Policy] = Seq(BouncerAccess())
|
|
||||||
private val finatraTestServiceIdentifiers: Seq[Policy] = Seq(
|
|
||||||
ServiceIdentifierPattern(
|
|
||||||
role = "",
|
|
||||||
service = "",
|
|
||||||
env = "",
|
|
||||||
zone = Seq(""))
|
|
||||||
)
|
|
||||||
|
|
||||||
override val policy: Policy = AnyOf(bouncerAccess ++ finatraTestServiceIdentifiers)
|
|
||||||
|
|
||||||
override type Key = gql.TimelineKey
|
|
||||||
override type View = gql.HomeTimelineView
|
|
||||||
override type Value = tr.Timeline
|
|
||||||
|
|
||||||
override val keyConv: Conv[Key] = ScroogeConv.fromStruct[gql.TimelineKey]
|
|
||||||
override val viewConv: Conv[View] = ScroogeConv.fromStruct[gql.HomeTimelineView]
|
|
||||||
override val valueConv: Conv[Value] = ScroogeConv.fromStruct[tr.Timeline]
|
|
||||||
|
|
||||||
private def createHomeMixerRequestArrow(
|
|
||||||
compositeOnUserClientColumn: CompositeOnUserClientColumn,
|
|
||||||
auditIpClientColumn: AuditIpClientColumn
|
|
||||||
): Arrow[(Key, View), hm.HomeMixerRequest] = {
|
|
||||||
|
|
||||||
val populateUserRolesAndIp: Arrow[(Key, View), (Option[Set[String]], Option[String])] = {
|
|
||||||
val gizmoduckView: (gd.LookupContext, Set[gd.QueryFields]) =
|
|
||||||
(gd.LookupContext(), Set(gd.QueryFields.Roles))
|
|
||||||
|
|
||||||
val populateUserRoles = Arrow
|
|
||||||
.flatMap[(Key, View), Option[Set[String]]] { _ =>
|
|
||||||
Stitch.collect {
|
|
||||||
CallContext.twitterUserId.map { userId =>
|
|
||||||
compositeOnUserClientColumn.fetcher
|
|
||||||
.callStack(HomeMixerColumn.FetchCallstack)
|
|
||||||
.fetch(userId, gizmoduckView).map(_.v)
|
|
||||||
.map {
|
|
||||||
_.flatMap(_.roles.map(_.roles.toSet)).getOrElse(Set.empty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val populateIpAddress = Arrow
|
|
||||||
.flatMap[(Key, View), Option[String]](_ =>
|
|
||||||
auditIpClientColumn.fetcher
|
|
||||||
.callStack(HomeMixerColumn.FetchCallstack)
|
|
||||||
.fetch((), ()).map(_.v))
|
|
||||||
|
|
||||||
Arrow.join(
|
|
||||||
populateUserRoles,
|
|
||||||
populateIpAddress
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrow.zipWithArg(populateUserRolesAndIp).map {
|
|
||||||
case ((key, view), (roles, ipAddress)) =>
|
|
||||||
val deviceContextOpt = Some(
|
|
||||||
hm.DeviceContext(
|
|
||||||
isPolling = CallContext.isPolling,
|
|
||||||
requestContext = view.requestContext,
|
|
||||||
latestControlAvailable = view.latestControlAvailable,
|
|
||||||
autoplayEnabled = view.autoplayEnabled
|
|
||||||
))
|
|
||||||
val seenTweetIds = view.seenTweetIds.filter(_.nonEmpty)
|
|
||||||
|
|
||||||
val (product, productContext) = key match {
|
|
||||||
case gql.TimelineKey.HomeTimeline(_) | gql.TimelineKey.HomeTimelineV2(_) =>
|
|
||||||
(
|
|
||||||
hm.Product.ForYou,
|
|
||||||
hm.ProductContext.ForYou(
|
|
||||||
hm.ForYou(
|
|
||||||
deviceContextOpt,
|
|
||||||
seenTweetIds,
|
|
||||||
view.dspClientContext,
|
|
||||||
view.pushToHomeTweetId
|
|
||||||
)
|
|
||||||
))
|
|
||||||
case gql.TimelineKey.HomeLatestTimeline(_) | gql.TimelineKey.HomeLatestTimelineV2(_) =>
|
|
||||||
(
|
|
||||||
hm.Product.Following,
|
|
||||||
hm.ProductContext.Following(
|
|
||||||
hm.Following(deviceContextOpt, seenTweetIds, view.dspClientContext)))
|
|
||||||
case gql.TimelineKey.CreatorSubscriptionsTimeline(_) =>
|
|
||||||
(
|
|
||||||
hm.Product.Subscribed,
|
|
||||||
hm.ProductContext.Subscribed(hm.Subscribed(deviceContextOpt, seenTweetIds)))
|
|
||||||
case _ => throw new UnsupportedOperationException(s"Unknown product: $key")
|
|
||||||
}
|
|
||||||
|
|
||||||
val clientContext = pm.ClientContext(
|
|
||||||
userId = CallContext.twitterUserId,
|
|
||||||
guestId = CallContext.guestId,
|
|
||||||
guestIdAds = CallContext.guestIdAds,
|
|
||||||
guestIdMarketing = CallContext.guestIdMarketing,
|
|
||||||
appId = CallContext.clientApplicationId,
|
|
||||||
ipAddress = ipAddress,
|
|
||||||
userAgent = CallContext.userAgent,
|
|
||||||
countryCode = CallContext.requestCountryCode,
|
|
||||||
languageCode = CallContext.requestLanguageCode,
|
|
||||||
isTwoffice = CallContext.isInternalOrTwoffice,
|
|
||||||
userRoles = roles,
|
|
||||||
deviceId = CallContext.deviceId,
|
|
||||||
mobileDeviceId = CallContext.mobileDeviceId,
|
|
||||||
mobileDeviceAdId = CallContext.adId,
|
|
||||||
limitAdTracking = CallContext.limitAdTracking
|
|
||||||
)
|
|
||||||
|
|
||||||
hm.HomeMixerRequest(
|
|
||||||
clientContext = clientContext,
|
|
||||||
product = product,
|
|
||||||
productContext = Some(productContext),
|
|
||||||
maxResults = Try(view.count.get.toInt).toOption.orElse(HomeMixerColumn.MaxCount),
|
|
||||||
cursor = view.cursor.filter(_.nonEmpty)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val fetch: Arrow[(Key, View), Result[Value]] = {
|
|
||||||
val transformThriftIntoPipelineRequest: Arrow[
|
|
||||||
(Key, View),
|
|
||||||
ProductPipelineRequest[HomeMixerRequest]
|
|
||||||
] = {
|
|
||||||
Arrow
|
|
||||||
.identity[(Key, View)]
|
|
||||||
.andThen {
|
|
||||||
createHomeMixerRequestArrow(compositeOnUserClientColumn, auditIpClientColumn)
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
case thriftRequest =>
|
|
||||||
val request = homeMixerRequestUnmarshaller(thriftRequest)
|
|
||||||
val params = paramsBuilder.build(
|
|
||||||
clientContext = request.clientContext,
|
|
||||||
product = request.product,
|
|
||||||
featureOverrides =
|
|
||||||
request.debugParams.flatMap(_.featureOverrides).getOrElse(Map.empty),
|
|
||||||
)
|
|
||||||
ProductPipelineRequest(request, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val underlyingProduct: Arrow[
|
|
||||||
ProductPipelineRequest[HomeMixerRequest],
|
|
||||||
ProductPipelineResult[tr.TimelineResponse]
|
|
||||||
] = Arrow
|
|
||||||
.identity[ProductPipelineRequest[HomeMixerRequest]]
|
|
||||||
.map { pipelineRequest =>
|
|
||||||
val pipelineArrow = productPipelineRegistry
|
|
||||||
.getProductPipeline[HomeMixerRequest, tr.TimelineResponse](
|
|
||||||
pipelineRequest.request.product)
|
|
||||||
.arrow
|
|
||||||
(pipelineArrow, pipelineRequest)
|
|
||||||
}.applyArrow
|
|
||||||
|
|
||||||
transformThriftIntoPipelineRequest.andThen(underlyingProduct).map {
|
|
||||||
_.result match {
|
|
||||||
case Some(result) => found(result.timeline)
|
|
||||||
case _ => missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object HomeMixerColumn {
|
|
||||||
val Path = "home-mixer/homeMixer.Timeline"
|
|
||||||
private val FetchCallstack = s"$Path:fetch"
|
|
||||||
private val MaxCount: Option[Int] = Some(100)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"3rdparty/jvm/javax/inject:javax.inject",
|
|
||||||
"finagle/finagle-memcached/src/main/scala",
|
|
||||||
"finatra/inject/inject-core/src/main/scala",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/param",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
|
|
||||||
"src/thrift/com/twitter/search:earlybird-scala",
|
|
||||||
"stitch/stitch-timelineservice/src/main/scala",
|
|
||||||
],
|
|
||||||
exports = [
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,44 +0,0 @@
|
|||||||
package com.twitter.home_mixer.functional_component.candidate_source
|
|
||||||
|
|
||||||
import com.twitter.product_mixer.core.feature.FeatureWithDefaultOnFailure
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMapBuilder
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSourceWithExtractedFeatures
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidatesWithSourceFeatures
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
|
|
||||||
import com.twitter.search.earlybird.{thriftscala => t}
|
|
||||||
import com.twitter.stitch.Stitch
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
case object EarlybirdResponseTruncatedFeature
|
|
||||||
extends FeatureWithDefaultOnFailure[t.EarlybirdRequest, Boolean] {
|
|
||||||
override val defaultValue: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
case object EarlybirdBottomTweetFeature
|
|
||||||
extends FeatureWithDefaultOnFailure[t.EarlybirdRequest, Option[Long]] {
|
|
||||||
override val defaultValue: Option[Long] = None
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
case class EarlybirdCandidateSource @Inject() (
|
|
||||||
earlybird: t.EarlybirdService.MethodPerEndpoint)
|
|
||||||
extends CandidateSourceWithExtractedFeatures[t.EarlybirdRequest, t.ThriftSearchResult] {
|
|
||||||
|
|
||||||
override val identifier = CandidateSourceIdentifier("Earlybird")
|
|
||||||
|
|
||||||
override def apply(
|
|
||||||
request: t.EarlybirdRequest
|
|
||||||
): Stitch[CandidatesWithSourceFeatures[t.ThriftSearchResult]] = {
|
|
||||||
Stitch.callFuture(earlybird.search(request)).map { response =>
|
|
||||||
val candidates = response.searchResults.map(_.results).getOrElse(Seq.empty)
|
|
||||||
|
|
||||||
val features = FeatureMapBuilder()
|
|
||||||
.add(EarlybirdResponseTruncatedFeature, candidates.size == request.searchQuery.numResults)
|
|
||||||
.add(EarlybirdBottomTweetFeature, candidates.lastOption.map(_.id))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
CandidatesWithSourceFeatures(candidates, features)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,30 +0,0 @@
|
|||||||
package com.twitter.home_mixer.functional_component.candidate_source
|
|
||||||
|
|
||||||
import com.google.inject.name.Named
|
|
||||||
import com.twitter.finagle.memcached.{Client => MemcachedClient}
|
|
||||||
import com.twitter.home_mixer.param.HomeMixerInjectionNames.StaleTweetsCache
|
|
||||||
import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource
|
|
||||||
import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier
|
|
||||||
import com.twitter.stitch.Stitch
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class StaleTweetsCacheCandidateSource @Inject() (
|
|
||||||
@Named(StaleTweetsCache) staleTweetsCache: MemcachedClient)
|
|
||||||
extends CandidateSource[Seq[Long], Long] {
|
|
||||||
|
|
||||||
override val identifier: CandidateSourceIdentifier = CandidateSourceIdentifier("StaleTweetsCache")
|
|
||||||
|
|
||||||
private val StaleTweetsCacheKeyPrefix = "v1_"
|
|
||||||
|
|
||||||
override def apply(request: Seq[Long]): Stitch[Seq[Long]] = {
|
|
||||||
val keys = request.map(StaleTweetsCacheKeyPrefix + _)
|
|
||||||
|
|
||||||
Stitch.callFuture(staleTweetsCache.get(keys).map { tweets =>
|
|
||||||
tweets.map {
|
|
||||||
case (k, _) => k.replaceFirst(StaleTweetsCacheKeyPrefix, "").toLong
|
|
||||||
}.toSeq
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
scala_library(
|
|
||||||
sources = ["*.scala"],
|
|
||||||
compiler_option_sets = ["fatal_warnings"],
|
|
||||||
strict_deps = True,
|
|
||||||
tags = ["bazel-compatible"],
|
|
||||||
dependencies = [
|
|
||||||
"finagle/finagle-core/src/main",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/decorator/builder",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/decorator/urt/builder",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/model",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/model/request",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/param",
|
|
||||||
"home-mixer/server/src/main/scala/com/twitter/home_mixer/util",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/decorator/urt",
|
|
||||||
"product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/model/candidate",
|
|
||||||
"product-mixer/core/src/main/java/com/twitter/product_mixer/core/product/guice/scope",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/decorator/urt/builder",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller/response/urt",
|
|
||||||
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline",
|
|
||||||
"src/scala/com/twitter/suggests/controller_data",
|
|
||||||
"src/thrift/com/twitter/suggests/controller_data:controller_data-scala",
|
|
||||||
"src/thrift/com/twitter/timelinescorer/common/scoredtweetcandidate:thrift-scala",
|
|
||||||
"src/thrift/com/twitter/timelineservice/server/suggests/logging:thrift-scala",
|
|
||||||
"stringcenter/client",
|
|
||||||
"stringcenter/client/src/main/java",
|
|
||||||
"timelines/src/main/scala/com/twitter/timelines/injection/scribe",
|
|
||||||
],
|
|
||||||
)
|
|
Binary file not shown.
Binary file not shown.
@ -1,51 +0,0 @@
|
|||||||
package com.twitter.home_mixer.functional_component.decorator
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.functional_component.decorator.builder.HomeConversationModuleMetadataBuilder
|
|
||||||
import com.twitter.home_mixer.functional_component.decorator.builder.HomeTimelinesScoreInfoBuilder
|
|
||||||
import com.twitter.home_mixer.functional_component.decorator.urt.builder.HomeFeedbackActionInfoBuilder
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures.ConversationModuleFocalTweetIdFeature
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.UrtItemCandidateDecorator
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.UrtMultipleModulesDecorator
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.item.tweet.TweetCandidateUrtItemBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.metadata.ClientEventInfoBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.timeline_module.StaticModuleDisplayTypeBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.decorator.urt.builder.timeline_module.TimelineModuleBuilder
|
|
||||||
import com.twitter.product_mixer.component_library.model.candidate.TweetCandidate
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.EntryNamespace
|
|
||||||
import com.twitter.product_mixer.core.model.marshalling.response.urt.timeline_module.VerticalConversation
|
|
||||||
import com.twitter.product_mixer.core.pipeline.PipelineQuery
|
|
||||||
import com.twitter.timelines.injection.scribe.InjectionScribeUtil
|
|
||||||
import com.twitter.timelineservice.suggests.{thriftscala => st}
|
|
||||||
|
|
||||||
object HomeConversationServiceCandidateDecorator {
|
|
||||||
|
|
||||||
private val ConversationModuleNamespace = EntryNamespace("home-conversation")
|
|
||||||
|
|
||||||
def apply(
|
|
||||||
homeFeedbackActionInfoBuilder: HomeFeedbackActionInfoBuilder
|
|
||||||
): Some[UrtMultipleModulesDecorator[PipelineQuery, TweetCandidate, Long]] = {
|
|
||||||
val suggestType = st.SuggestType.RankedOrganicTweet
|
|
||||||
val component = InjectionScribeUtil.scribeComponent(suggestType).get
|
|
||||||
val clientEventInfoBuilder = ClientEventInfoBuilder(component)
|
|
||||||
val tweetItemBuilder = TweetCandidateUrtItemBuilder(
|
|
||||||
clientEventInfoBuilder = clientEventInfoBuilder,
|
|
||||||
timelinesScoreInfoBuilder = Some(HomeTimelinesScoreInfoBuilder),
|
|
||||||
feedbackActionInfoBuilder = Some(homeFeedbackActionInfoBuilder)
|
|
||||||
)
|
|
||||||
|
|
||||||
val moduleBuilder = TimelineModuleBuilder(
|
|
||||||
entryNamespace = ConversationModuleNamespace,
|
|
||||||
clientEventInfoBuilder = clientEventInfoBuilder,
|
|
||||||
displayTypeBuilder = StaticModuleDisplayTypeBuilder(VerticalConversation),
|
|
||||||
metadataBuilder = Some(HomeConversationModuleMetadataBuilder())
|
|
||||||
)
|
|
||||||
|
|
||||||
Some(
|
|
||||||
UrtMultipleModulesDecorator(
|
|
||||||
urtItemCandidateDecorator = UrtItemCandidateDecorator(tweetItemBuilder),
|
|
||||||
moduleBuilder = moduleBuilder,
|
|
||||||
groupByKey = (_, _, candidateFeatures) =>
|
|
||||||
candidateFeatures.getOrElse(ConversationModuleFocalTweetIdFeature, None)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -1,18 +0,0 @@
|
|||||||
package com.twitter.home_mixer.functional_component.decorator
|
|
||||||
|
|
||||||
import com.twitter.home_mixer.model.HomeFeatures._
|
|
||||||
import com.twitter.product_mixer.core.feature.featuremap.FeatureMap
|
|
||||||
|
|
||||||
object HomeQueryTypePredicates {
|
|
||||||
private[this] val QueryPredicates: Seq[(String, FeatureMap => Boolean)] = Seq(
|
|
||||||
("request", _ => true),
|
|
||||||
("get_initial", _.getOrElse(GetInitialFeature, false)),
|
|
||||||
("get_newer", _.getOrElse(GetNewerFeature, false)),
|
|
||||||
("get_older", _.getOrElse(GetOlderFeature, false)),
|
|
||||||
("pull_to_refresh", _.getOrElse(PullToRefreshFeature, false)),
|
|
||||||
("request_context_launch", _.getOrElse(IsLaunchRequestFeature, false)),
|
|
||||||
("request_context_foreground", _.getOrElse(IsForegroundRequestFeature, false))
|
|
||||||
)
|
|
||||||
|
|
||||||
val PredicateMap = QueryPredicates.toMap
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user