Table of Contents
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:
-
Use non-blocking methods and handle notifications (the reactor pattern).
-
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.
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.
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:
-
Create a
BackgroundTask.Manager
. -
Use the
BackgroundTask.Manager
to start background threads. -
Wrap blocking calls into a
BackgroundTask
subclass. -
Add
BackgroundTask
s to theBackgroundTask.Manager
. -
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
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 )