PowerTAC Demo Agent Documentation

From PowerTAC
Revision as of 19:23, 23 July 2013 by Grampajohn (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Overview

This material has been obsolete since early 2011.

This document is a guide to help developers use the PowerTAC Demo Agent - GRAILS (PDA-G) as a foundation to develop an agent for the PowerTAC platform.

This is written under the assumption that the developer is familiar with Java and Groovy. PDA-G is developed using the GRAILS framework to take advance of its support in web-based application. Web-based user interface is used to view agent status and control agent behavior.

Advantages of using PDA-G for developing an agent:

  • Easy to add support for new messages from server
  • Easy to register of message handlers
  • Easy to add phase activation tasks
  • Easy to send message to server

The main goals are flexibility and ease of use. These would allow developers to concentrate on agent domain-specific behaviors without dealing with low level details in the communication layer.

There are two main events that could trigger agent action:

  1. A particular event from the server. This is in form of JMS messages coming from the server.
  2. Phase activation. This is a way to introduce periodic trigger for agent to perform processing at the beginning of each timeslot. Phase is used to maintain processing order of the registered tasks. Agent can register tasks to be performed in each phase. The number of phases in PDA-G is defined in the configuration file. Timeslot is kept in synch with server via server time management messages. System (agent's host OS) time is required to be managed with Network Time Protocol (NTP) service to maintain synchronization with server time.

An additional source of event could be from interaction with the agent's web-based UI.

Communication with server

Agent makes web service (WS) call to authenticate with the server using username and password. The WS call will response with URL to JMS server and agent's JMS destination name (queue name). After login, communication between server and agent is over JMS messages.

Structure

 + grails-app/
   + conf/
     - BuildConfig.groovy
     - Config.groovy
     + spring/
       - resources.groovy
   + controllers/                                                   Agent's web controllers
   + domain/                                                        Agent's domain classes
   + i18n/
   + jobs/
   + views/
   + services/                                                      Agent's services
 + src/
   + java/
   + groovy/
     + api/                                                         API classes
     + core/                                                        Core package
     + exceptions/                                                  Exceptions classes
     + infrastructure/                                              Messaging and persistence infrastructure
     + interfaces/                                                  Interfaces 
 + test/
   + unit/
   + integration/
 + web-app/


Each component of an agent is wired up using Spring allow easy replacement of different implementation in resource file grails-app/conf/spring/resources.groovy. For example, default messagePersistenceManager uses hibernate/GORM to manage persistence. Developer could implement persistence manager that store data using a different method and replace messagePersistenceManager bean with the new class. Similar concept applies to other agent components.

Note: Auto-wiring is done by GRAILS on GRAILS controllers, services, jobs but not in classes in 'src' directory. Auto-wiring for classes in 'src' directory is accomplished by 'resource.groovy'

grails-app/conf/spring/resources.groovy

beans = {
  // JMS related bean (brokerURL is set after establishing JMS provider URL from server) 
  jmsConnectionFactory(org.springframework.jms.connection.SingleConnectionFactory) {bean ->
    targetConnectionFactory = {org.apache.activemq.ActiveMQConnectionFactory cf ->
      brokerURL = ''
    }
  }
 
  // Message Listener registration manager
  messageListenerRegistrar(org.powertac.broker.infrastructure.messaging.MessageListenerRegistrar)

  // XML-Message receiver 
  xmlMessageReceiver(org.powertac.broker.infrastructure.messaging.XMLMessageReceiver) {
    messageListenerRegistrar = messageListenerRegistrar

    // using same XStream based message converter from powertac-common plugin
    messageConverter = ref('messageConverter')
  }

  // Message Receiver receives JMS message and lets the specific JMS message type handler to handle
  // the received message.  Currently PDA-G only handles xml messages wrapped in JMS TextMessage(s).
  messageReceiver(org.powertac.broker.infrastructure.messaging.MessageReceiver) {
    xmlMessageReceiver = xmlMessageReceiver
  }

  // Processor to dispatch individual messages within a list as bundled by the server in one JMS message.
  arrayListProcessor(org.powertac.broker.infrastructure.messaging.ArrayListProcessor) { bean ->
    messageListenerRegistrar = messageListenerRegistrar
  }

  // Wire Message Persistence Manager to persist messages.  PDA-G uses GORM-based implementation by default.
  messagePersistenceManager(org.powertac.broker.infrastructure.persistence.GormBasedMessagePersistenceManager)

  // Injecting the message persistence manager for the listener to use
  marketMessageListener(org.powertac.broker.infrastructure.messaging.MarketMessageListener) { bean ->
    messagePersistenceManager = messagePersistenceManager
  }

  // Wire tariff negotiator 
  tariffNegotiator(org.powertac.broker.core.tariffnegotiator.DemoTariffNegotiator)
}

Getting started

Checking out agent source code

From github

  1. cd /your/target/checkout/directory
  2. git clone git://github.com/powertac/powertac-demo-agent-grails.git

Repeat Step 2 for

  1. git clone git://github.com/powertac-plugins/powertac-common.git
  2. git clone git://github.com/powertac-plugins/powertac-db-stuff.git

Running agent in developement mode

Note: currently, all dependency modules have to be checkout as sub-directory under one directory.

1) Start an instance of the server (instruction on how to download server at [1])

% cd powertac-server
% grails run-app

2) Start an instance of the agent (specify server.port so it would not conflict with server if server is running on the same machine)

% cd powertac-demo-agent-grails
% grails -Dserver.port=9090 run-app

3) Browse to http://localhost:9090/powertac-demo-agent-grails and perform login for agent to server

4) Browse to http://localhost:8080/powertac-server to get to server web dashbard to perform server function (e.g. start competition)

Registering to event from server

The simplest method to register to an event (message) from server is to implement MessageListenerWithAutoRegistration interface. The implementation would have a getMessages method that returns a list of interested message types and an onMessage for each of the interested message type. An example is MarketMessageListener.

Registering to phase activation

The simplest method to register with phase activation is to implement TimeslotPhaseProcessorWithAutoRegistration interface. The implementation would have a getPhases method that returns a list of interested phase and an activate method that take in a Instant object and a phase number that would be call when each of the interested phase is activated. An example is ShoutRequestService.

Sample code

MarketMessageListener

/**
 * This class handles all incoming market messages.
 */
class MarketMessageListener implements MessageListenerWithAutoRegistration
{
  private static final log = LogFactory.getLog(this)

  // Auto-wired by Spring (in resource.groovy)
  def messagePersistenceManager

  // Message types that do not need to be persisted
  static final def transientClazz = [SimEnd, ArrayList, CustomerBootstrapData,
      CustomerList, ErrorCmd, SimStart, SimPause, SimResume]

  /**
   * Returns a list of the interested message types.  
   * Interested message type would also include its own type and its all of
   * child types. Hence, interested message type of Object would include all
   * message types. As part of the auto registration mechanism, this method
   * is called during agent initialization process,
   * right after agent successfully login to the server.
   */
  def getMessages () {
    [Object]
  }
  
  /**
   * Handler for the specific message type. This method is called when incoming message
   * from market match with the parameter type. The parameter type has to be one of 
   * interested message type or one of its descendants to be called.
   */
  def onMessage (Object msg) {
    log.debug("onMessage(${msg.class.name}) - start")
    if (!transientClazz.contains(msg.class)) {
      log.debug("onMessage(${msg.class.name}) - saving...")
      messagePersistenceManager.save(msg)
    }
    log.debug("onMessage(${msg.class.name}) - end")
  }
}

GormBasedMessagePersistenceManager

/**
 * This class persists message to database using GORM/Hibernate.  Using GORM/Hibernate to persist
 * JMS message from server introduces complication in saving data to avoid stale-object problem
 * with Hibernate.  GORM/Hibernate uses version field in the entity for optimistic lock checking.
 * Primary key generation mechanism is defined in the entity definition and primary key is established
 * by the entity producer where this is sometimes at the server lead to complication with primary key
 * at the agent when agent receive the message.
 * These issues cause higher complexity in code that persist the messages to DB.    
 */
class GormBasedMessagePersistenceManager
{

  private static final log = LogFactory.getLog(this)

  // length of timeslot in milliseconds, reinitialized at the beginning of each game
  private long timeslotMillis

  def save (Object obj) {
    log.warn("I don't know how to save ${obj.class.name} message type yet")
  }

  def save (WeatherReport wr) {
    log.debug("save(WeatherReport) - start")
    wr.merge()
    log.debug("save(WeatherReport) - end")
  }

  def save (Orderbook ob) {
    log.debug("save(Orderbook) - start")

    def asks = ob.asks.clone()
    def bids = ob.bids.clone()

    ob.asks.clear()
    ob.bids.clear()

    def timeslot = ob.timeslot
    if (timeslot) {
      timeslot.addToOrderbooks(ob)
    }

    ob.save()

    def processedAsks = []
    asks.each { ask ->
      def dbAsk = OrderbookAsk.findById(ask.id)
      if (dbAsk) {
        ask.version = dbAsk.version
        ask = ask.merge()
      }
      processedAsks << ask
    }

    def processedBids = []
    bids.each { bid ->
      def dbBid = OrderbookBid.findById(bid.id)
      if (dbBid) {
        bid.version = dbBid.version
        bid = bid.merge()
      }
      processedBids << bid
    }

    ob.bids.addAll(processedBids)
    ob.asks.addAll(processedAsks)

    log.debug("save(Orderbook) - end")
  }

  def save (CustomerInfo ci) {
    log.debug("save(CustomerInfo) - start")
    ci.merge()
    log.debug("save(CustomerInfo) - end")
  }

  def save (BankTransaction bt) {
    log.debug("save(BankTransaction) - begin")

    bt.merge()

    log.debug("save(BankTransaction) - end")
  }

  def save (CashPosition cp) {
    log.debug("save(CashPosition) - begin")

    def dbCp = CashPosition.findByBroker(cp.broker)
    if (dbCp) {
      log.debug("save(CashPosition - found in db")
      cp.id = dbCp.id
      cp.version = dbCp.version
      cp.merge(flush: true)
    } else {
      log.debug("save(CashPosition - NOT found in db")
      cp.id = null
      cp.save(flush: true)
    }
    log.debug("save(CashPosition) - XXXX broker ${cp?.broker?.username} has ${cp?.balance}")

    log.debug("save(CashPosition) - end")
  }

  def save (ClearedTrade ct) {
    log.debug("save(ClearedTrade) - start")

    def dbCt = ClearedTrade.findById(ct.id)
    if (dbCt) {
      ct.version = dbCt.version
    }

    ct.merge()

    log.debug("save(ClearedTrade) - end")
  }

  def save (TariffSpecification ts) {
    log.debug("save(TariffSpecification) - start")

    def processedRates = []

    ts.rates.each { rate ->
      def dbRate = Rate.findById(rate.id)
      if (dbRate) {
        log.debug("save(TariffSpecification) - found [dbRate.id:${dbRate.id},dbRate.version:${dbRate.version},rate.version:${rate.version}]")
        rate.version = dbRate.version
        rate = rate.merge()
        log.debug("save(TariffSpecification) - after merge rate:${rate}")
      } else {
        log.debug("save(TariffSpecification) - not found rate:${rate.id}")
        rate.save()
      }
      processedRates << rate
    }

    log.debug("save(TariffSpecification) - there are ${ts.rates.size()} item in ts.rates")
    log.debug("save(TariffSpecification) - there are ${processedRates.size()} item in processedRates")

    ts.rates.clear()
    ts.rates.addAll(processedRates)

    if (TariffSpecification.findById(ts.id)) {
      ts.merge()
    } else {
      ts.save()
    }

    log.debug("save(TariffSpecification) - end")
  }

  def save (TariffTransaction ttx) {
    log.debug("save(TariffTransaction) - start")

    ttx.merge()

    log.debug("save(TariffTransaction) - receving ${ttx.txType} ttx for ${ttx.broker.username}")
    log.debug("save(TariffTransaction) - end")

  }

  def save (TariffStatus ts) {
    log.debug("save(TariffStatus) - start")

    ts.merge()

    log.debug("save(TariffStatus) - start")
  }


  def save (Competition competition) {
    log.debug("save(Competition) - start")

    competition.brokers?.each {
      log.debug("save(Competition) - populate broker: ${it}")
      def broker = new Broker(username: it, enabled: true)
      broker.save()
    }

    timeslotMillis = competition.timeslotLength * TimeService.MINUTE

    log.debug("save(Competition) - saving competition ${competition}:${competition.save() ? 'successful' : competition.errors}")
    log.debug("save(Competition) - end")
  }

  def save (SimStart simStart) {
    log.debug("save(SimStart) - start")
    log.debug("Saving simStart - start @ ${simStart.start}")

    simStart.save()

    log.debug("save(SimStart) - this: ${this}")
    log.debug("save(SimStart) - end")
  }

  def save (TimeslotUpdate slotUpdate) {
    log.debug("save(TimeslotUpdate) - start")

    log.debug("save(TimeslotUpdate) - received TimeslotUpdate: ${slotUpdate.id}")

    def newEnableds = []
    slotUpdate.enabled?.each {
      it.id = it.serialNumber
      it.enabled = true
      it.endInstant = it.startInstant + timeslotMillis
      log.debug("save(TimeslotUpdate) -    saving enabled timeslot ${it.id}: ${(newEnableds << it.merge()) ? 'successful' : it.errors}")
    }
    slotUpdate.enabled = newEnableds

    def newDisables = []
    slotUpdate.disabled?.each {
      def dbTimeslot = Timeslot.findBySerialNumber(it.serialNumber)
      it.id = it.serialNumber
      it.enabled = false
      it.endInstant = it.startInstant + timeslotMillis
      it.version = dbTimeslot.version
      log.debug("save(TimeslotUpdate) -    saving disabled timeslot ${it.id}: ${(newDisables << it.merge()) ? 'successful' : it.errors}")
    }
    slotUpdate.disabled = newDisables

    log.debug("save(TimeslotUpdate) - saving TimeslotUpdate ${slotUpdate.id}:${slotUpdate.save() ? 'successful' : slotUpdate.errors}")

    log.debug("save(TimeslotUpdate) - end")
  }

  def save (BalancingTransaction bt) {
    log.debug("save(BalancingTransaction) - not yet implemented")
  }

  def save (DistributionTransaction dt) {
    log.debug("save(DistributionTransaction) - not yet implemented")
  }
}

ShoutRequestService

/**
 * This class sends shout at each timeslot based on pre-determined 
 * shout requests.
 */
class ShoutRequestService implements TimeslotPhaseProcessorWithAutoRegistration
{
  static transactional = true

  // Auto-wired by GRAILS
  def jmsManagementService

  /**
   * Returns a list of interested phases. The infrastructure layer would
   * call the "activate" method at the interested phases. As part of the
   * auto registration mechanism, this method is called during agent 
   * initialization process, right after agent successfully login
   * to the server.
   */
  def getPhases() {
    [1]
  }

  /**
   * Activation method.  This method is called at the interested phase 
   * activation processing in the beginning of each timeslot.
   * @param time The current simulation time.
   * @param phaseNumber The activated phase number.
   */
  void activate (Instant time, int phaseNumber) {
    Timeslot timeslot = getTimeSlot(time)
    if (timeslot) {
      def shouts = ShoutRequest.findAllActiveAtTimeslot(timeslot.serialNumber).list()
      shouts?.each { shoutRequest ->
        log.debug("activate -    shout: ${shoutRequest}")
        def shout = new Shout(shoutRequest.properties)
        shout.broker = Broker.findByUsername(ConfigurationHolder.config.powertac.username)
        shout.timeslot = getTimeSlot(time)
        jmsManagementService.send(shout)
      }
    }
  }

  /**
   * Determines time slot number from current simulation time.
   */
  def getTimeSlot(time) {
    def competition = Competition.currentCompetition()
    def timeslot = null
    if (competition) {
      def startTime = competition.simulationBaseTime
      int numTimeslots = (time.millis - startTime.millis) / (competition.timeslotLength * TimeService.MINUTE)
      log.debug("startTime: ${startTime}, currentTime: ${time}, numTimeslots: ${numTimeslots}")
      timeslot = new Timeslot(serialNumber: numTimeslots + 1)
    }
    return timeslot
  }
}

Reference material

./services/org/powertac/broker/GameStateService.groovy                        Service for retrieve/update GameState 
./services/org/powertac/broker/PauseActionStateService.groovy                 Service for pausing game functionality
./services/org/powertac/broker/TimeslotPhaseService.groovy                    Service for time slot phase activation
./services/org/powertac/broker/CashPositionService.groovy                     Service for retrieve/update CashPosition
./services/org/powertac/broker/ConnectionService.groovy                       Service for connection management
./services/org/powertac/broker/JmsManagementService.groovy                    Service for JMS management
./services/org/powertac/broker/AutoLoginService.groovy                        Service for auto login functionality
./services/org/powertac/broker/ShoutRequestService.groovy                     Service for requesting shout
./services/org/powertac/broker/CompetitionManagementService.groovy            Service for managing competition
./services/org/powertac/broker/TariffPublishingService.groovy                 Service for publishing tarifff to server
./jobs/org/powertac/broker/LoginJob.groovy                                    Task for auto login
./jobs/org/powertac/broker/ClockDriveJob.groovy                               Task for time slot management
./conf/UrlMappings.groovy                                                     Url mapping configuration file
./conf/BootStrap.groovy                                                       Bootstrap file
./conf/Config.groovy                                                          Project configuration file
./conf/DataSource.groovy                                                      Datasource configuration file
./conf/BuildConfig.groovy                                                     Build/dependency configuration file
./conf/QuartzConfig.groovy                                                    Timer service configuration file
./conf/spring/resources.groovy                                                Spring resource file
./domain/org/powertac/broker/ShoutRequest.groovy                              Domain class for ShoutRequest
./domain/org/powertac/broker/PauseActionState.groovy                          Domain class for PauseActionState
./domain/org/powertac/broker/GameState.groovy                                 Domain class for GameState
./controllers/org/powertac/broker/ShoutRequestController.groovy               Web controller for ShoutRequest
./controllers/org/powertac/broker/StatusController.groovy                     Web controller for conntection/login status
./controllers/org/powertac/broker/TariffPublisherController.groovy            Web controller for tarriff publishing
./controllers/org/powertac/broker/ConnectionController.groovy                 Web controller for connection management
./controllers/org/powertac/broker/GameStatusController.groovy                 Web controller for game status

FAQ

  • How to run a non-web-based agent?
 The web interface is only there to help with ease of agent development.  Agents could be developed without using the web interface. 
  • What relevant source files control the demo agent's behaviour?
 Agent behavior can be controlled with both asynchronous events and periodic events.
 * Asynchronous events come from messages from the server such as TariffSpecification, Orderbook, etc.
 * Periodic events come from registered tasks to be executed for each timeslot.
  • Any other recommendations for getting started tuning / developing an

agent?

 Kang, P. "Software Architecture of the TAC Energy Trading Broker", August 5, 2010