github.com/Laplace-Game-Development/Laplace-Entangled-Environment@v0.0.3/internal/schedule/tasks.go (about) 1 package schedule 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "strings" 8 "syscall" 9 "time" 10 11 "github.com/Laplace-Game-Development/Laplace-Entangled-Environment/internal/data" 12 "github.com/Laplace-Game-Development/Laplace-Entangled-Environment/internal/event" 13 "github.com/Laplace-Game-Development/Laplace-Entangled-Environment/internal/policy" 14 "github.com/Laplace-Game-Development/Laplace-Entangled-Environment/internal/redis" 15 "github.com/Laplace-Game-Development/Laplace-Entangled-Environment/internal/zeromq" 16 "github.com/mediocregopher/radix/v3" 17 "github.com/pebbe/zmq4" 18 ) 19 20 //// Configurables 21 22 // Number of Workers to Distribute to with ZeroMQ 23 const NumberOfTaskWorkers uint8 = 10 24 25 // Proxy Publish Port for sending to (application) 26 const ProxyFEPort string = ":5565" 27 28 // Proxy Sub Port for receiving From (workers) 29 const ProxyBEPort string = ":5566" 30 31 // Proxy Control Port for Interrupting Proxy 32 const ProxyControlPort string = ":5567" 33 34 // Time that a Worker should sleep/wait in the event 35 // of no tasks being ready 36 const EmptyQueueSleepDuration time.Duration = time.Duration(time.Second * time.Duration(NumberOfTaskWorkers)) 37 38 // Time that a worker should spend waiting to receive 39 // for new work 40 const RecvIOTimeoutDuration time.Duration = time.Duration(time.Microsecond) 41 42 // Number of Event Health Tasks to pop off of redis 43 // Maybe this should be in schedule.go 44 const EventHealthTaskCapacity uint8 = 50 45 46 // Labeled MagicRune as a joke for Golangs named type 47 // Used as seperator for communicating work to workers 48 // Task Name/Prefix + MagicRune + Params/Data 49 const MagicRune rune = '~' 50 51 //// Task Name/Prefixes 52 53 // Game Health Checking to garbage collect game data 54 const HealthTaskPrefix string = "healthTask" 55 56 // Unit Testing Prefix for adding to the database using workers 57 const TestTaskPrefix string = "unitTest0" 58 59 //// Global Variables | Singletons 60 61 // Control Communication for Workers Input 62 // Used For Cleanup 63 var zeroMQWorkerChannelIn chan bool = nil 64 65 // Control Communication for Workers Output 66 // Used For Cleanup 67 var zeroMQWorkerChannelOut chan bool = nil 68 69 // ServerTask Startup Function for Schedule Task System. Creates Workers 70 // and communication channels with ZeroMQ. 71 func StartTaskQueue() (func(), error) { 72 // FOR CI: 73 // TODO, it may be easier to bind these to ports assigned by the database 74 // TODO, alternatively we could use smart configuration generators 75 76 // Start Asynchronous proxy (costs 1 thread) 77 proxyResponse := make(chan bool) 78 79 go startAsynchronousProxy(proxyResponse) 80 81 response := <-proxyResponse 82 83 if !response { 84 return nil, errors.New("Could not Start Proxy (Check Logs!)") 85 } 86 87 // TODO Buffer to numberOfTaskWorkers 88 zeroMQWorkerChannelIn = make(chan bool) 89 zeroMQWorkerChannelOut = make(chan bool) 90 91 // Start a few workers 92 for i := uint8(0); i < NumberOfTaskWorkers; i++ { 93 go startTaskWorker(i, zeroMQWorkerChannelIn, zeroMQWorkerChannelOut) 94 } 95 96 return cleanUpTaskQueue, nil 97 } 98 99 // CleanUp Function returned by Startup function. Signals Workers to Finish and 100 // blocks until completion. 101 func cleanUpTaskQueue() { 102 log.Println("Signalling Task Workers for CleanUp") 103 104 for i := uint8(0); i < NumberOfTaskWorkers; i++ { 105 zeroMQWorkerChannelIn <- true 106 } 107 108 log.Println("Signalling Finished Waiting for response!") 109 for i := uint8(0); i < NumberOfTaskWorkers; i++ { 110 <-zeroMQWorkerChannelOut 111 } 112 113 log.Println("Task Workers Cleanup Complete!") 114 115 log.Println("Clearing Proxy") 116 117 // Set a Control 118 zeroMQProxyControl, err := zeromq.MainZeroMQ.NewSocket(zmq4.Type(zmq4.REQ)) 119 if err != nil { 120 log.Fatalf("Error Clearing Proxy! Err: %v\n", err) 121 } 122 123 err = zeroMQProxyControl.Bind(zeromq.ZeromqMask + ProxyControlPort) 124 if err != nil { 125 log.Fatalf("Error Clearing Proxy! Err: %v\n", err) 126 } 127 128 zeroMQProxyControl.Send("TERMINATE", zmq4.Flag(0)) 129 zeroMQProxyControl.Recv(zmq4.Flag(0)) 130 131 log.Println("Clearing Proxy Complete!") 132 } 133 134 /////////////////////////////////////////////////////////////////////////////////////////////////// 135 //// 136 //// Task Queue Preparation 137 //// 138 /////////////////////////////////////////////////////////////////////////////////////////////////// 139 140 // Starts the Proxy Communication Layer for a 0MQ Queue Device. 141 // The Application communicates to this block the tasks and data that need 142 // to be worked on. The Proxy then distributes 143 // the data using ZeroMQ's distribution algorithms. 144 // 145 // resp :: boolean channel which is used to return success/failure 146 // (since the worker loops forever) 147 // 148 // Should be called in a goroutine otherwise zmq_proxy will block the main "thread" 149 func startAsynchronousProxy(resp chan bool) { 150 // Worker-Facing Publisher/ROUTER 151 zmqXREP, err := zeromq.MainZeroMQ.NewSocket(zmq4.Type(zmq4.ROUTER)) 152 if err != nil { 153 log.Println(err) 154 resp <- false 155 return 156 } 157 158 err = zmqXREP.Bind(zeromq.ZeromqMask + ProxyFEPort) 159 if err != nil { 160 log.Println(err) 161 resp <- false 162 return 163 } 164 165 // Callsite socket 166 zmqXREQ, err := zeromq.MainZeroMQ.NewSocket(zmq4.Type(zmq4.DEALER)) 167 if err != nil { 168 log.Println(err) 169 resp <- false 170 return 171 } 172 173 err = zmqXREQ.Bind(zeromq.ZeromqMask + ProxyBEPort) 174 if err != nil { 175 log.Println(err) 176 resp <- false 177 return 178 } 179 180 // Interrupt socket 181 // Use a Router here for STATISTICS as a potential return rather than 182 // documentation's reported SUB socket. 183 zmqCON, err := zeromq.MainZeroMQ.NewSocket(zmq4.Type(zmq4.REP)) 184 if err != nil { 185 log.Println(err) 186 resp <- false 187 return 188 } 189 190 err = zmqCON.Connect(zeromq.ZeromqHost + ProxyControlPort) 191 if err != nil { 192 log.Println(err) 193 resp <- false 194 return 195 } 196 197 resp <- true 198 199 // Will Run Forever 200 err = zmq4.ProxySteerable(zmqXREQ, zmqXREP, nil, zmqCON) 201 if err != nil { 202 // This cannot be Fatal otherwise test will fail 203 log.Printf("Proxy Ended with Error! Err: %v\n", err) 204 } 205 206 log.Println("Proxy Signaled To Be Terminated!") 207 zmqCON.Send("OK", zmq4.Flag(0)) 208 209 zmqXREQ.Close() 210 zmqXREP.Close() 211 zmqCON.Close() 212 } 213 214 // Starts a worker with the given ID. The worker receives and consumes 215 // work from the proxy. It then performs the required event if it recongizes 216 // the Task Name/Prefix. 217 // 218 // Any errors in parsing or working the task is logged. 219 // 220 // id :: numerical id for logging 221 // signalChannel :: channel for control signal for cleanup 222 // responseChannel :: an output channel when a signal is received for cleanup 223 // 224 // The function is contructed to loop forever until signaled otherwise. 225 // This function should be run with a goroutine. 226 func startTaskWorker(id uint8, signalChannel chan bool, responseChannel chan bool) { 227 // Startup 228 subSocket, err := zmq4.NewSocket(zmq4.Type(zmq4.REP)) 229 if err != nil { 230 log.Printf("Could Not Start Task Worker! ID: %d\n", id) 231 log.Println(err) 232 return 233 } 234 235 err = subSocket.Connect(zeromq.ZeromqHost + ProxyBEPort) 236 if err != nil { 237 log.Printf("Could Not Start Task Worker! ID: %d\n", id) 238 log.Println(err) 239 return 240 } 241 242 err = subSocket.SetRcvtimeo(time.Duration(time.Second)) 243 if err != nil { 244 log.Printf("Could Not Start Task Worker! ID: %d\n", id) 245 log.Println(err) 246 return 247 } 248 249 // Consume Tasks 250 for { 251 msg, err := subSocket.Recv(zmq4.Flag(zmq4.DONTWAIT)) 252 if err != nil && zmq4.AsErrno(err) == zmq4.Errno(syscall.EAGAIN) { 253 log.Printf("Nothing to Consume! ID: %d\n", id) 254 time.Sleep(EmptyQueueSleepDuration) 255 } else if err != nil { 256 log.Printf("Error Upon Consuming! ID: %d\n", id) 257 log.Println(err) 258 259 log.Printf("Cleaning Up Thread ID: %d\n", id) 260 return 261 } else { 262 subSocket.Send("OK", zmq4.DONTWAIT) 263 onTask(msg) 264 subSocket.Send("DONE", zmq4.DONTWAIT) 265 } 266 267 select { 268 case <-signalChannel: 269 responseChannel <- true 270 log.Printf("Cleaning Up Thread ID: %d\n", id) 271 subSocket.Close() 272 return 273 default: 274 } 275 } 276 277 } 278 279 /////////////////////////////////////////////////////////////////////////////////////////////////// 280 //// 281 //// Tasks Communication -- Communicating Tasks and Data to Proxy 282 //// 283 /////////////////////////////////////////////////////////////////////////////////////////////////// 284 285 // Function used by schedule.go to communicate the scheduled tasks to the workers. 286 // This takes an array of Task Name/Prefixes+MagicRune+Data strings and sends them 287 // to the proxy (which in turn send them to the workers.). 288 // 289 // msg :: slice of string messages to send to workers (Task Name/Prefix+MagicRune+Data) 290 // 291 // Returns an Error if communicating fails (server-shutoff or the full message could 292 // not be sent). 293 func SendTasksToWorkers(msgs ...string) error { 294 // Proxy Facing Publisher 295 zmqREQ, err := zeromq.MainZeroMQ.NewSocket(zmq4.Type(zmq4.DEALER)) 296 if err != nil { 297 return err 298 } 299 300 log.Println("Connecting to Worker Proxy!") 301 err = zmqREQ.Connect(zeromq.ZeromqHost + ProxyFEPort) 302 if err != nil { 303 return err 304 } 305 306 count := 0 307 for _, msg := range msgs { 308 if len(msg) == 0 { 309 break 310 } 311 312 if count > 0 { 313 time.Sleep(time.Second) 314 } 315 316 log.Println("Sending Delimitter") 317 _, err := zmqREQ.Send("", zmq4.SNDMORE) 318 if err != nil { 319 return err 320 } 321 322 log.Printf("Sending Message: %s\n", msg) 323 num, err := zmqREQ.Send(msg, zmq4.Flag(0)) 324 if err != nil { 325 return err 326 } else if len(msg) != num { 327 return errors.New("ZeroMQ did not Accept Full Job! Characters Accepted:" + fmt.Sprintf("%d", num)) 328 } 329 330 count += 1 331 } 332 333 log.Println("Waiting for Confirmations") 334 for i := 0; i < count; i++ { 335 _, err = zmqREQ.Recv(zmq4.Flag(0)) 336 if err != nil { 337 return err 338 } 339 log.Printf("Confirmation Received!") 340 } 341 log.Println("Received all Confirmations") 342 343 log.Println("Closing connection to Worker Proxy!") 344 err = zmqREQ.Close() 345 if err != nil { 346 return err 347 } 348 349 return nil 350 } 351 352 /////////////////////////////////////////////////////////////////////////////////////////////////// 353 //// 354 //// Tasks Working -- Parsed Worker Functions 355 //// 356 /////////////////////////////////////////////////////////////////////////////////////////////////// 357 358 // Map of Strings to their specific events. The Functions are "work" functions which load the 359 // required data to call a function in the event module. These are called and used by workers. 360 var mapPrefixToWork map[string]func([]string) error = map[string]func([]string) error{ 361 HealthTaskPrefix: healthTaskWork, 362 TestTaskPrefix: testTaskWork, 363 } 364 365 // Parses the given message and runs the associated function based on "mapPrefixToWork" 366 // Parsing errors are returned as an error. 367 // 368 // msg :: string message for working (Task Name/Prefix+MagicRune+Data) 369 func onTask(msg string) error { 370 log.Println("Got Message! | " + msg) 371 if len(msg) <= 0 { 372 log.Println("Message was empty!") 373 return nil 374 } 375 376 task, args := parseTask(msg) 377 378 work, exists := mapPrefixToWork[task] 379 380 if !exists { 381 return errors.New("Unknown Task Sent to Task Worker! MSG: " + msg) 382 } 383 384 return work(args) 385 } 386 387 // Performs the loading required for game garbage collection. It then calls 388 // the Game Health Check Event with the proper args. 389 // 390 // args :: the data from the msg. This should be a 391 func healthTaskWork(args []string) error { 392 if len(args) < 1 { 393 return errors.New("Task Did Not Receive Game ID!") 394 } 395 396 gameTime, err := data.GetRoomHealth(args[0]) 397 if err != nil { 398 return err 399 } 400 401 if gameTime.Add(policy.StaleGameDuration).Before(time.Now().UTC()) { 402 superUserRequest, err := policy.RequestWithSuperUser(true, policy.CmdGameDelete, data.SelectGameArgs{GameID: args[0]}) 403 if err != nil { 404 return err 405 } 406 407 resp := data.DeleteGame(superUserRequest.Header, superUserRequest.BodyFactories, superUserRequest.IsSecureConnection) 408 if resp.ServerError != nil { 409 return resp.ServerError 410 } 411 412 } else { 413 event.SubmitGameForHealthCheck(args[0]) 414 } 415 416 return nil 417 } 418 419 // Adds a given set of arguments to the redis database for testing 420 // 421 // args:: the data from the msg 422 // args[0] :: redis Set Key 423 // args[1] :: string value 424 // 425 // Only For Unit Testing 426 func testTaskWork(args []string) error { 427 if len(args) < 2 { 428 return errors.New("Task Did Not Receive Set Key and Value!") 429 } 430 431 log.Printf("Unit Test Work Running!\nAdding %s to %s\n", args[0], args[1]) 432 return redis.MainRedis.Do(radix.Cmd(nil, "SADD", args[0], args[1])) 433 } 434 435 /////////////////////////////////////////////////////////////////////////////////////////////////// 436 //// 437 //// Tasks Utility Functions -- Functions that help when creating and submitting tasks 438 //// 439 /////////////////////////////////////////////////////////////////////////////////////////////////// 440 441 // Constructs a string by joining the prefix and args with magic runes 442 // i.e. result = prefix + MagicRune + arg0 + MagicRune + arg1 + [MagicRune + argN] ... 443 func constructTaskWithPrefix(prefix string, args ...string) string { 444 var builder strings.Builder 445 446 builder.WriteString(prefix) 447 448 for _, s := range args { 449 builder.WriteRune(MagicRune) 450 builder.WriteString(s) 451 } 452 453 return builder.String() 454 } 455 456 // Parses a Task based on a MagicRune delimited string. 457 // Returns the Prefix and the slice of MagicRune Delimited args. 458 func parseTask(msg string) (string, []string) { 459 slice := strings.Split(msg, string(MagicRune)) 460 return slice[0], slice[1:] 461 }