// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import Foundation
import Combine
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
public enum ConfigurationSyncJob : JobExecutor {
public static let maxFailureCount : Int = - 1
public static let requiresThreadId : Bool = true
public static let requiresInteractionId : Bool = false
private static let maxRunFrequency : TimeInterval = 3
private static let waitTimeForExpirationUpdate : TimeInterval = 1
public static func run (
_ job : Job ,
queue : DispatchQueue ,
success : @ escaping ( Job , Bool , Dependencies ) -> ( ) ,
failure : @ escaping ( Job , Error ? , Bool , Dependencies ) -> ( ) ,
deferred : @ escaping ( Job , Dependencies ) -> ( ) ,
using dependencies : Dependencies
) {
guard Identity . userCompletedRequiredOnboarding ( ) else { return success ( job , true , dependencies ) }
// I t ' s p o s s i b l e f o r m u l t i p l e C o n f i g S y n c J o b ' s w i t h t h e s a m e t a r g e t ( u s e r / g r o u p ) t o t r y t o r u n a t t h e
// s a m e t i m e s i n c e a s s o o n a s o n e i s s t a r t e d w e w i l l e n q u e u e a s e c o n d o n e , r a t h e r t h a n a d d i n g d e p e n d e n c i e s
// b e t w e e n t h e j o b s w e j u s t c o n t i n u e t o d e f e r t h e s u b s e q u e n t j o b w h i l e t h e f i r s t o n e i s r u n n i n g i n
// o r d e r t o p r e v e n t m u l t i p l e c o n f i g u r a t i o n S y n c j o b s w i t h t h e s a m e t a r g e t f r o m r u n n i n g a t t h e s a m e t i m e
guard
dependencies
. jobRunner
. jobInfoFor ( state : . running , variant : . configurationSync )
. filter ( { key , info in
key != job . id && // E x c l u d e t h i s j o b
info . threadId = = job . threadId // E x c l u d e j o b s f o r d i f f e r e n t i d s
} )
. isEmpty
else {
// D e f e r t h e j o b t o r u n ' m a x R u n F r e q u e n c y ' f r o m w h e n t h i s o n e r a n ( i f w e d o n ' t i t ' l l t r y s t a r t
// i t a g a i n i m m e d i a t e l y w h i c h i s p o i n t l e s s )
let updatedJob : Job ? = dependencies . storage . write { db in
try job
. with ( nextRunTimestamp : dependencies . dateNow . timeIntervalSince1970 + maxRunFrequency )
. saved ( db )
}
SNLog ( " [ConfigurationSyncJob] For \( job . threadId ? ? " UnknownId " ) deferred due to in progress job " )
return deferred ( updatedJob ? ? job , dependencies )
}
// I f w e d o n ' t h a v e a u s e r K e y P a i r y e t t h e n t h e r e i s n o n e e d t o s y n c t h e c o n f i g u r a t i o n
// a s t h e u s e r d o e s n ' t e x i s t y e t ( t h i s w i l l g e t t r i g g e r e d o n t h e f i r s t l a u n c h o f a
// f r e s h i n s t a l l d u e t o t h e m i g r a t i o n s g e t t i n g r u n )
guard
let publicKey : String = job . threadId ,
let pendingChanges : LibSession . PendingChanges = dependencies . storage
. read ( using : dependencies , { db in try LibSession . pendingChanges ( db , publicKey : publicKey ) } )
else {
SNLog ( " [ConfigurationSyncJob] For \( job . threadId ? ? " UnknownId " ) failed due to invalid data " )
return failure ( job , StorageError . generic , false , dependencies )
}
// I f t h e r e a r e n o p e n d i n g c h a n g e s t h e n t h e j o b c a n j u s t c o m p l e t e ( n e x t t i m e s o m e t h i n g
// i s u p d a t e d w e w a n t t o t r y a n d r u n i m m e d i a t e l y s o d o n ' t s c u e d u l e a n o t h e r r u n i n t h i s c a s e )
guard ! pendingChanges . pushData . isEmpty || ! pendingChanges . obsoleteHashes . isEmpty else {
SNLog ( " [ConfigurationSyncJob] For \( publicKey ) completed with no pending changes " )
return success ( job , true , dependencies )
}
let jobStartTimestamp : TimeInterval = dependencies . dateNow . timeIntervalSince1970
let messageSendTimestamp : Int64 = SnodeAPI . currentOffsetTimestampMs ( )
SNLog ( " [ConfigurationSyncJob] For \( publicKey ) started with \( pendingChanges . pushData . count ) change \( pendingChanges . pushData . count = = 1 ? " " : " s " ) , \( pendingChanges . obsoleteHashes . count ) old hash \( pendingChanges . obsoleteHashes . count = = 1 ? " " : " es " ) " )
dependencies . storage
. readPublisher { db in
try SnodeAPI . preparedSequence (
requests : try pendingChanges . pushData
. map { pushData -> ErasedPreparedRequest in
try SnodeAPI . preparedSendMessage (
db ,
message : SnodeMessage (
recipient : publicKey ,
data : pushData . data . base64EncodedString ( ) ,
ttl : pushData . variant . ttl ,
timestampMs : UInt64 ( messageSendTimestamp )
) ,
in : pushData . variant . namespace ,
using : dependencies
)
}
. appending ( try {
guard ! pendingChanges . obsoleteHashes . isEmpty else { return nil }
return try SnodeAPI . preparedDeleteMessages (
db ,
swarmPublicKey : publicKey ,
serverHashes : Array ( pendingChanges . obsoleteHashes ) ,
requireSuccessfulDeletion : false ,
using : dependencies
)
} ( ) ) ,
requireAllBatchResponses : false ,
swarmPublicKey : publicKey ,
using : dependencies
)
}
. flatMap { $0 . send ( using : dependencies ) }
. subscribe ( on : queue )
. receive ( on : queue )
. map { ( _ : ResponseInfoType , response : Network . BatchResponse ) -> [ ConfigDump ] in
// / T h e n u m b e r o f r e s p o n s e s r e t u r n e d m i g h t n o t m a t c h t h e n u m b e r o f c h a n g e s s e n t b u t t h e y w i l l b e r e t u r n e d
// / i n t h e s a m e o r d e r , t h i s m e a n s w e c a n j u s t ` z i p ` t h e t w o a r r a y s a s i t w i l l t a k e t h e s m a l l e r o f t h e t w o a n d
// / c o r r e c t l y a l i g n t h e r e s p o n s e t o t h e c h a n g e
zip ( response , pendingChanges . pushData )
. compactMap { ( subResponse : Any , pushData : LibSession . PendingChanges . PushData ) in
// / I f t h e r e q u e s t w a s n ' t s u c c e s s f u l t h e n j u s t i g n o r e i t ( t h e n e x t t i m e w e s y n c t h i s c o n f i g w e w i l l t r y
// / t o s e n d t h e c h a n g e s a g a i n )
guard
let typedResponse : Network . BatchSubResponse < SendMessagesResponse > = ( subResponse as ? Network . BatchSubResponse < SendMessagesResponse > ) ,
200. . . 299 ~= typedResponse . code ,
! typedResponse . failedToParseBody ,
let sendMessageResponse : SendMessagesResponse = typedResponse . body
else { return nil }
// / S i n c e t h i s c h a n g e w a s s u c c e s s f u l w e n e e d t o m a r k i t a s p u s h e d a n d g e n e r a t e a n y c o n f i g d u m p s
// / w h i c h n e e d t o b e s t o r e d
return LibSession . markingAsPushed (
seqNo : pushData . seqNo ,
serverHash : sendMessageResponse . hash ,
sentTimestamp : messageSendTimestamp ,
variant : pushData . variant ,
publicKey : publicKey
)
}
}
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
case . finished : SNLog ( " [ConfigurationSyncJob] For \( publicKey ) completed " )
case . failure ( let error ) :
SNLog ( " [ConfigurationSyncJob] For \( publicKey ) failed due to error: \( error ) " )
failure ( job , error , false , dependencies )
}
} ,
receiveValue : { ( configDumps : [ ConfigDump ] ) in
// F l a g t o i n d i c a t e w h e t h e r t h e j o b s h o u l d b e f i n i s h e d o r w i l l r u n a g a i n
var shouldFinishCurrentJob : Bool = false
// L a s t l y w e n e e d t o s a v e t h e u p d a t e d d u m p s t o t h e d a t a b a s e
let updatedJob : Job ? = dependencies . storage . write { db in
// S a v e t h e u p d a t e d d u m p s t o t h e d a t a b a s e
try configDumps . forEach { try $0 . save ( db ) }
// W h e n w e c o m p l e t e t h e ' C o n f i g u r a t i o n S y n c ' j o b w e w a n t t o i m m e d i a t e l y s c h e d u l e
// a n o t h e r o n e w i t h a ' n e x t R u n T i m e s t a m p ' s e t t o t h e ' m a x R u n F r e q u e n c y ' v a l u e t o
// t h r o t t l e t h e c o n f i g s y n c r e q u e s t s
let nextRunTimestamp : TimeInterval = ( jobStartTimestamp + maxRunFrequency )
// I f a n o t h e r ' C o n f i g u r a t i o n S y n c ' j o b w a s s c h e d u l e d t h e n u p d a t e t h a t o n e
// t o r u n a t ' n e x t R u n T i m e s t a m p ' a n d m a k e t h e c u r r e n t j o b s t o p
if
let existingJob : Job = try ? Job
. filter ( Job . Columns . id != job . id )
. filter ( Job . Columns . variant = = Job . Variant . configurationSync )
. filter ( Job . Columns . threadId = = publicKey )
. order ( Job . Columns . nextRunTimestamp . asc )
. fetchOne ( db )
{
// I f t h e n e x t j o b i s n ' t c u r r e n t l y r u n n i n g t h e n d e l a y i t ' s s t a r t t i m e
// u n t i l t h e ' n e x t R u n T i m e s t a m p '
if ! dependencies . jobRunner . isCurrentlyRunning ( existingJob ) {
_ = try existingJob
. with ( nextRunTimestamp : nextRunTimestamp )
. saved ( db )
}
// I f t h e r e i s a n o t h e r j o b t h e n w e s h o u l d f i n i s h t h i s o n e
shouldFinishCurrentJob = true
return job
}
return try job
. with ( nextRunTimestamp : nextRunTimestamp )
. saved ( db )
}
success ( ( updatedJob ? ? job ) , shouldFinishCurrentJob , dependencies )
}
)
}
}
// MARK: - C o n v e n i e n c e
public extension ConfigurationSyncJob {
static func enqueue (
_ db : Database ,
publicKey : String ,
dependencies : Dependencies = Dependencies ( )
) {
// U p s e r t a c o n f i g s y n c j o b i f n e e d e d
dependencies . jobRunner . upsert (
db ,
job : ConfigurationSyncJob . createIfNeeded ( db , publicKey : publicKey , using : dependencies ) ,
canStartJob : true ,
using : dependencies
)
}
@ discardableResult static func createIfNeeded (
_ db : Database ,
publicKey : String ,
using dependencies : Dependencies = Dependencies ( )
) -> Job ? {
// / T h e C o n f i g u r a t i o n S y n c J o b w i l l a u t o m a t i c a l l y r e s c h e d u l e i t s e l f t o r u n a g a i n a f t e r 3 s e c o n d s s o i f t h e r e i s a n e x i s t i n g
// / j o b t h e n t h e r e i s n o n e e d t o c r e a t e a n o t h e r i n s t a n c e
// /
// / * * N o t e : * * J o b s w i t h d i f f e r e n t ` t h r e a d I d ` v a l u e s c a n r u n c o n c u r r e n t l y
guard
dependencies . jobRunner
. jobInfoFor ( state : . running , variant : . configurationSync )
. filter ( { _ , info in info . threadId = = publicKey } )
. isEmpty ,
( try ? Job
. filter ( Job . Columns . variant = = Job . Variant . configurationSync )
. filter ( Job . Columns . threadId = = publicKey )
. isEmpty ( db ) )
. defaulting ( to : false )
else { return nil }
// O t h e r w i s e c r e a t e a n e w j o b
return Job (
variant : . configurationSync ,
behaviour : . recurring ,
threadId : publicKey
)
}
static func run ( using dependencies : Dependencies = Dependencies ( ) ) -> AnyPublisher < Void , Error > {
// T r i g g e r t h e j o b e m i t t i n g t h e r e s u l t w h e n c o m p l e t e d
return Deferred {
Future { resolver in
ConfigurationSyncJob . run (
Job ( variant : . configurationSync ) ,
queue : . global ( qos : . userInitiated ) ,
success : { _ , _ , _ in resolver ( Result . success ( ( ) ) ) } ,
failure : { _ , error , _ , _ in resolver ( Result . failure ( error ? ? NetworkError . unknown ) ) } ,
deferred : { _ , _ in } ,
using : dependencies
)
}
}
. eraseToAnyPublisher ( )
}
}