bw logo

Chapter 15. External Services

From your game script you may want to access external services such as a billing or shopping system. When doing so, it is important not to block on I/O as a process that pauses for too long may be considered as dead by other server components. To avoid blocking on I/O you can either:

  1. Use non-blocking methods and handle notifications (the reactor pattern).

  2. Call blocking methods from a background thread (the thread pool pattern).

When available, non-blocking methods is preferred over background threads.

Note

Due to the Python interpreter's implementation, the main thread could still be blocked if a background thread does not release the Global Interpreter Lock (GIL) frequently enough. By default a thread automatically releases the GIL every 100 bytecode instructions. The GIL is not automatically released when C code is called, the C code has the responsibility of periodically releasing the GIL. Please be aware that some Python modules are simply C API bindings.

15.1. Non-blocking Methods

The easiest way to avoid blocking on I/O is to use non-blocking methods and handle notifications. You can register a callback to be invoked once a file descriptor has characters to be read with the BigWorld.registerFileDescriptor method. Similarly, you can register a callback to be invoked once a file descriptor becomes writable with the BigWorld.registerWriteFileDescriptor method. Both methods are respectively complemented with the BigWorld.deregisterFileDescriptor and BigWorld.deregisterWriteFileDescriptor methods. For more information please see the BaseApp and CellApp API documentation.

15.2. Background Threads

To call a blocking method in a background thread you need to use the BackgroundTask module (this module is available from the file BackgroundTask.py in the import path bigworld/res/scripts/server_common). This module is available to both BaseApp and CellApp script. Below is a summary of usage:

  1. Create a BackgroundTask.Manager.

  2. Use the BackgroundTask.Manager to start background threads.

  3. Wrap blocking calls into a BackgroundTask subclass.

  4. Add BackgroundTasks to the BackgroundTask.Manager.

  5. Use the BackgroundTask.Manager to stop background threads.

BigWorld ships with an example to illustrate the usage of this background thread behaviour in the NoteDataStore example located in the FantasyDemo resource tree. This example can be found in the directory fantasydemo/res/server/examples/note_data_store. This example connects with an external database (ie: external to the DBMgr entity database), in order to store abitrary notes from a client.

The remainder of this section describes how to add a row to an external database in a BackgroundTask as illustrated through the NoteDataStore example.

import BackgroundTask
import sqlalchemy

bgTaskMgr = None

def init( config_file ):
  ...
  bgTaskMgr = BackgroundTask.BgTaskManager()
  bgTaskMgr.startThreads( 5 ) # Can optionally pass a functor to create thread data per thread.
  
def fini():
  ...
  bgTaskMgr.stopAll()
  
class Note( sqlalchemy.SQLAlchemyBase ):
  ... 

fantasydemo/res/scripts/base/NoteDataStore.py

In the code above, the init method creates a BackgroundTask.Manager (step 1) and start background threads (step 2) while the fini method stops all background threads (step 5).

class AddNoteTask( BackgroundTask ):
  def __init__( self, noteReporter, description ):
    self.noteReporter = noteReporter
    self.note = NoteDataStore.Note( description )

  def doBackgroundTask( self, bgTaskMgr, threadData ):
    session = create_session()
    session.add( self.note )
    session.flush()                          # Blocking method

    bgTaskMgr.addMainThreadTask( self )      # Re-add ourself to invoke the callback in the main thread

  def doMainThreadTask( self, bgTaskMgr ):   # Invoke the callback
    ...
    self.noteReporter.onAddNote( id )

class NoteReporter( object ):
  def addNote( self, description ):
    ...
    task = AddNoteTask( self, description )
    NoteDataStore.bgTaskMgr.addBackgroundTask( task )

  def onAddNote( self, id ):                   # AddNoteTask's callback
    ...

fantasydemo/res/scripts/base/NoteReporter.py

In the code above, the AddNoteTask subclass wraps the blocking method session.flush (step 3). The method NoteReporter.addNote creates an AddNoteTask instance and adds it to the BackgroundTask.Manager as a background thread task (step 4). The AddNoteTask instance will invoke the callback NoteReporter.onAddNote after it finishes its background thread work. To invoke a callback, overload the BackgroundTask.doMainThreadTask method and inside BackgroundTask.doBackgroundTask make the subclass re-add itself to the BackgroundTask.Manager as a main thread task.

Note

The BackgroundTask module is a Python port of the C++ version located at bigworld/src/lib/cstdmf/bgtask_manager.hpp

15.2.1. Caveats

Due to Python's thread implementation and the way that BigWorld incorporates Python, it is unsafe to perform any modifications on entities from within a background thread. For example, the following code would be considered unsafe:

class DatabaseTask( BackgroundTask ):

  ...

  def doBackgroundTask( self, bgTaskMgr, threadData ):
    # Interact with DB to fetch data
    
    self.entity.cell.applyData( dataFromDB )

It is unsafe to call the cell.applyData() method as the Python thread context may switch back to the main thread and send a corrupt network packet.

A safe / correct approach to avoid these kind of issues would be the following:

class DatabaseTask( BackgroundTask ):

  ...

  def doBackgroundTask( self, bgTaskMgr, threadData ):
    # Interact with DB to fetch data
    self.dataFromDB = dataFromDB
    bgTaskMgr.addMainThreadTask( self )


  def doMainThreadTask( self, bgTaskMgr ):
    self.entity.cell.applyData( self.dataFromDB )