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  }