edu.utexas.cs.tactex.core.PowerTacBroker.java Source code

Java tutorial

Introduction

Here is the source code for edu.utexas.cs.tactex.core.PowerTacBroker.java

Source

/*
 * TacTex - a power trading agent that competed in the Power Trading Agent Competition (Power TAC) www.powertac.org
 * Copyright (c) 2013-2016 Daniel Urieli and Peter Stone {urieli,pstone}@cs.utexas.edu               
 *
 *
 * This file is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This file is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * This file incorporates work covered by the following copyright and  
 * permission notice:  
 *
 *     Copyright 2012-2013 the original author or authors.
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an
 *     "AS IS" BASIS,  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 *     either express or implied. See the License for the specific language
 *     governing permissions and limitations under the License.
 */
package edu.utexas.cs.tactex.core;

import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.log4j.Logger;
import org.joda.time.Instant;
import org.powertac.common.Broker;
import org.powertac.common.Competition;
import org.powertac.common.CustomerInfo;
import org.powertac.common.IdGenerator;
import org.powertac.common.TimeService;
import org.powertac.common.Timeslot;
import org.powertac.common.config.ConfigurableValue;
import org.powertac.common.msg.BrokerAccept;
import org.powertac.common.msg.BrokerAuthentication;
import org.powertac.common.msg.SimEnd;
import org.powertac.common.msg.SimPause;
import org.powertac.common.msg.SimResume;
import org.powertac.common.msg.SimStart;
import org.powertac.common.msg.TimeslotComplete;
import org.powertac.common.msg.TimeslotUpdate;
import org.powertac.common.repo.BrokerRepo;
import org.powertac.common.repo.CustomerRepo;
import org.powertac.common.repo.DomainRepo;
import org.powertac.common.repo.TimeslotRepo;
import org.powertac.common.spring.SpringApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import edu.utexas.cs.tactex.PortfolioManagerService;
import edu.utexas.cs.tactex.interfaces.Activatable;
import edu.utexas.cs.tactex.interfaces.BrokerContext;
import edu.utexas.cs.tactex.interfaces.Initializable;

/**
 * This is the top-level controller for the broker. It sets up the other
 * components, maintains the clock, and terminates when the SimEnd message
 * is received.
 * 
 * @author John Collins
 */
@Service
public class PowerTacBroker implements BrokerContext {
    static private Logger log = Logger.getLogger(PowerTacBroker.class);

    @Autowired
    private BrokerPropertiesService propertiesService;

    @Autowired
    private TimeService timeService;

    @Autowired
    private TimeslotRepo timeslotRepo;

    @Autowired
    private BrokerRepo brokerRepo;

    // Broker components
    @Autowired
    private MessageDispatcher router;

    @Autowired
    private JmsManagementService jmsManagementService;

    @Autowired
    private BrokerTournamentService brokerTournamentService;

    @Autowired
    private BrokerMessageReceiver brokerMessageReceiver;

    @Autowired
    private CustomerRepo customerRepo;

    /** parameters */
    // keep in mind that brokers need to deal with two viewpoints. Tariff
    // types take the viewpoint of the customer, while market-related types
    // take the viewpoint of the broker.

    @ConfigurableValue(valueType = "Integer", description = "length of customer usage records")
    private Integer usageRecordLength = 7 * 24; // one week

    @ConfigurableValue(valueType = "Integer", description = "Login retry timeout in msec")
    private Integer loginRetryTimeout = 3000; // 3 sec

    @ConfigurableValue(valueType = "Integer", description = "Time limit in msec to retry logins before giving up")
    private Integer retryTimeLimit = 180000; // 3 min

    @ConfigurableValue(valueType = "String", description = "Broker username")
    private String username = "broker";

    @ConfigurableValue(valueType = "String", description = "Broker login password")
    private String password = "password";

    @ConfigurableValue(valueType = "String", description = "Name of tournament")
    private String tourneyName = "";

    @ConfigurableValue(valueType = "String", description = "url for tournament login")
    private String tourneyUrl = "";

    @ConfigurableValue(valueType = "String", description = "Authorization token for tournament")
    private String authToken = "";

    // Broker keeps its own records
    //private ArrayList<String> brokerNames;
    //private Instant baseTime = null;
    private long quittingTime = 0l;
    private int currentTimeslot = 0; // index of last started timeslot
    private int timeslotCompleted = 0; // index of last completed timeslot
    private int pausedAt = 0; // index of current timeslot during pause, else 0
    private boolean running = false; // true to run, false to stop
    private BrokerAdapter adapter;
    private String serverQueueName = "serverInput";
    private String brokerQueueName = null; // set by tournament manager

    // synchronization variables
    private boolean noNtp = false; // true if we should attempt offset estimate
    private long brokerTime = 0l;
    private long serverClockOffset = 0l; // should stay zero for ntp situation
    //private long maxResponseDelay = 400l; // <800msec delay is "immediate"
    private long defaultResponseTime = 100l;

    // needed for backward compatibility
    private String jmsBrokerUrl = null;

    /**
     * Default constructor for remote broker deployment
     */
    public PowerTacBroker() {
        super();
    }

    /**
     * Starts a new session
     * @param noNtp 
     */
    public void startSession(File configFile, String jmsUrl, boolean noNtp, String queueName, String serverQueue,
            long end) {
        quittingTime = end;
        this.noNtp = noNtp;
        if (null != queueName && !queueName.isEmpty())
            brokerQueueName = queueName;
        if (null != serverQueue && !serverQueue.isEmpty())
            serverQueueName = serverQueue;
        if (null != configFile && configFile.canRead())
            propertiesService.setUserConfig(configFile);
        if (null != jmsUrl)
            propertiesService.setProperty("edu.utexas.cs.tactex.core.jmsManagementService.jmsBrokerUrl", jmsUrl);
        propertiesService.configureMe(this);

        // Initialize and run.
        init();
        run();
    }

    /**
     * Sets up the "adapter" broker, initializes the other services, registers
     * for incoming messages.
     */
    @SuppressWarnings("unchecked")
    public void init() {
        // initialize repos
        List<DomainRepo> repos = SpringApplicationContext.listBeansOfType(DomainRepo.class);
        log.debug("found " + repos.size() + " repos");
        for (DomainRepo repo : repos) {
            repo.recycle();
        }

        // set up the adapter
        adapter = new BrokerAdapter(username);
        brokerRepo.add(adapter); // to resolve incoming messages correctly

        // initialize services
        List<Initializable> initializers = SpringApplicationContext.listBeansOfType(Initializable.class);
        for (Initializable svc : initializers) {
            svc.initialize(this);
            registerMessageHandlers(svc);
        }

        // register message handlers for the broker core also
        registerMessageHandlers(this);
    }

    /**
     * Finds all the handleMessage() methdods and registers them.
     */
    private void registerMessageHandlers(Object thing) {
        Class<?> thingClass = thing.getClass();
        Method[] methods = thingClass.getMethods();
        for (Method method : methods) {
            if (method.getName().equals("handleMessage")) {
                Class<?>[] args = method.getParameterTypes();
                if (1 == args.length) {
                    log.info("Register " + thing.getClass().getSimpleName() + ".handleMessage("
                            + args[0].getSimpleName() + ")");
                    router.registerMessageHandler(thing, args[0]);
                }
            }
        }
    }

    /**
     * Logs in and waits for the sim to end.
     */
    public void run() {
        if (null == brokerQueueName)
            brokerQueueName = username;
        // log into the tournament manager if tourneyUrl is non-empty
        if (null != tourneyUrl && !tourneyUrl.isEmpty()
                && brokerTournamentService.login(tourneyName, tourneyUrl, authToken, quittingTime)) {
            jmsBrokerUrl = brokerTournamentService.getJmsUrl();
            brokerQueueName = brokerTournamentService.getBrokerQueueName();
            serverQueueName = brokerTournamentService.getServerQueueName();
        }

        // wait for the JMS broker to show up and create our queue
        adapter.setQueueName(brokerQueueName);
        // if null, assume local broker without jms connectivity
        jmsManagementService.init(jmsBrokerUrl, serverQueueName);
        jmsManagementService.registerMessageListener(brokerMessageReceiver, brokerQueueName);
        log.info("Listening on queue " + brokerQueueName);

        // Log in to server.
        // In case the server does not respond within  second
        BrokerAuthentication auth = new BrokerAuthentication(username, password);
        synchronized (this) {
            long now = new Date().getTime();
            while (!adapter.isEnabled() && (new Date().getTime() - now) < retryTimeLimit) {
                try {
                    brokerTime = new Date().getTime();
                    auth.setBrokerTime(brokerTime);
                    sendMessage(auth);
                    wait(loginRetryTimeout);
                } catch (InterruptedException e) {
                    log.warn("Interrupted!");
                    break;
                } catch (Exception ex) {
                    log.info("log attempt failed " + ex.toString());
                    try {
                        Thread.sleep(loginRetryTimeout);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                }
            }
        }
        if (!adapter.isEnabled()) {
            jmsManagementService.shutdown();
            return;
        }

        // start the activation thread
        AgentRunner runner = new AgentRunner(this);
        runner.start();
        try {
            runner.join();
        } catch (InterruptedException ie) {
            log.warn("Interrupted!");
        }
        jmsManagementService.shutdown();
    }

    // ------------- Accessors ----------------
    /**
     * Returns the "real" broker underneath this monstrosity
     */
    @Override
    public Broker getBroker() {
        return adapter;
    }

    /**
     * Returns the username for this broker
     */
    @Override
    public String getBrokerUsername() {
        return adapter.getUsername();
    }

    /**
     * Returns the simulation base time
     */
    @Override
    public Instant getBaseTime() {
        return timeService.getBaseInstant();
    }

    /**
     * Returns the length of the standard data array (24h * 7d)
     */
    @Override
    public int getUsageRecordLength() {
        return usageRecordLength;
    }

    /**
     * Returns the broker's list of competing brokers - non-public
     */
    @Override
    public List<String> getBrokerList() {
        return brokerRepo.findRetailBrokerNames();
    }

    /**
     * Returns the computed server time offset after login. Value is
     * positive if the server's clock is ahead (shows a later time) of the
     * broker's clock.
     */
    public long getServerTimeOffset() {
        return serverClockOffset;
    }

    /**
     * Delegates registrations to the router
     */
    @Override
    public void registerMessageHandler(Object handler, Class<?> messageType) {
        router.registerMessageHandler(handler, messageType);
    }

    // ------------ process messages -------------
    /**
     * Incoming messages for brokers include:
     * <ul>
     * <li>TariffTransaction tells us about customer subscription
     *   activity and power usage,</li>
     * <li>MarketPosition tells us how much power we have bought
     *   or sold in a given timeslot,</li>
     * <li>CashPosition tells us it's time to send in our bids/asks</li>
     * </ul>
     */

    /**
     * Sends an outgoing message. May need to be reimplemented in a remote broker.
     */
    @Override
    public void sendMessage(Object message) {
        if (message != null) {
            router.sendMessage(message);
        }
    }

    // -------------------- message handlers ---------------------
    //
    // Note that these arrive in JMS threads; If they share data with the
    // agent processing thread, they need to be synchronized.

    /**
     * BrokerAccept comes out when our authentication credentials are accepted
     * and we become part of the game. Before this, we cannot send any messages
     * other than BrokerAuthentication. Also, note that the ID prefix needs to be
     * set before any server-visible entities are created (such as tariff specs).
     * 
     * Here we estimate the timeoffset between broker and server. This is
     * slightly complicated because in a non-tournament situation the
     * login request might have been sent before the server was prepared
     * to deal with it. So if the response delay seems out of range,
     * we ignore the earlier time.
     */
    public synchronized void handleMessage(BrokerAccept accept) {
        adapter.setEnabled(true);
        // set up prefix and keys
        IdGenerator.setPrefix(accept.getPrefix());
        adapter.setKey(accept.getKey());
        router.setKey(accept.getKey());
        // estimate time offset
        long now = new Date().getTime();
        long response = now - brokerTime;
        //if (response < maxResponseDelay) {
        // assume the response was halfway between login and now
        //  now -= response / 2;
        //}
        //else {
        // assume default response time
        now -= defaultResponseTime;
        //}
        if (noNtp) {
            // Rough clock offset calculation
            if (0l != accept.getServerTime()) {
                // ignore missing data for backward compatibility
                serverClockOffset = accept.getServerTime() - now;
                if (Math.abs(serverClockOffset) < defaultResponseTime) {
                    // assume ntp is working
                    serverClockOffset = 0l;
                }
            } else {
                log.info("Server does not provide system time - cannot adjust offset");
            }
            log.info("login response = " + response + ", server clock offset = " + serverClockOffset);
        }
        notifyAll();
    }

    /**
     * Handles the Competition instance that arrives at beginning of game.
     * Here we capture all the customer records so we can keep track of their
     * subscriptions and usage profiles.
     */
    public void handleMessage(Competition comp) {
        // comp needs to be the "current competition"
        Competition.setCurrent(comp);

        // record the customers and brokers
        for (CustomerInfo customer : comp.getCustomers()) {
            customerRepo.add(customer);
        }
        for (String brokerName : comp.getBrokers()) {
            if (!(brokerName.equals(adapter.getUsername()))) {
                Broker competitor = new Broker(brokerName);
                log.info("adding competitor " + brokerName);
                brokerRepo.add(competitor);
            }
        }
        // in a remote broker, we pull out the clock
        // parameters to init the local clock, and create the initial timeslots.
        Instant bootBaseTime = comp.getSimulationBaseTime();
        int bootTimeslotCount = (int) (comp.getBootstrapTimeslotCount() + comp.getBootstrapDiscardedTimeslots());
        // now set time to end of bootstrap period.
        timeService.setClockParameters(comp.getClockParameters());
        timeService.init(bootBaseTime.plus(bootTimeslotCount * comp.getTimeslotDuration()));
        log.info("Sim start time: " + timeService.getCurrentDateTime().toString());
    }

    /**
     * Receives the SimPause message, used to pause the clock.
     * While the clock is paused, the broker needs to ignore the local clock.
     */
    public void handleMessage(SimPause sp) {
        // local brokers can ignore this.
        log.info("Paused at " + timeService.getCurrentDateTime().toString());
        pausedAt = timeslotRepo.currentSerialNumber();
    }

    /**
     * Receives the SimResume message, used to update the clock.
     */
    public void handleMessage(SimResume sr) {
        // local brokers don't need to handle this
        log.info("Resumed");
        pausedAt = 0;
        timeService.setStart(sr.getStart().getMillis() - serverClockOffset);
        timeService.updateTime();
    }

    /**
     * Receives the SimStart message, used to start the clock. The
     * server's clock offset is subtracted from the start time indicated
     * by the server.
     */
    public void handleMessage(SimStart ss) {
        log.info("SimStart - start time is " + ss.getStart().toString());
        timeService.setStart(ss.getStart().getMillis() - serverClockOffset);
        timeService.updateTime();
        log.info("SimStart - clock set to " + timeService.getCurrentDateTime().toString());
    }

    /**
     * Receives the SimEnd message, which ends the broker session.
     */
    public synchronized void handleMessage(SimEnd se) {
        log.info("SimEnd received");
        running = false;
        notifyAll();
    }

    /**
     * Updates the sim clock on receipt of the TimeslotUpdate message,
     * which should be the first to arrive in each timeslot. We have to disable
     * all the timeslots prior to the first enabled slot, then create and enable
     * all the enabled slots.
     */
    public synchronized void handleMessage(TimeslotUpdate tu) {
        Timeslot old = timeslotRepo.currentTimeslot();
        timeService.updateTime(); // here is the clock update
        log.info("TimeslotUpdate at " + timeService.getCurrentDateTime().toString());
        //List<Timeslot> enabled = tu.getEnabled();
        for (int index = old.getSerialNumber(); index < tu.getFirstEnabled(); index++) {
            timeslotRepo.findOrCreateBySerialNumber(index);
            currentTimeslot = index;
        }
        for (int index = tu.getFirstEnabled(); index <= tu.getLastEnabled(); index++) {
            timeslotRepo.findOrCreateBySerialNumber(index);
        }
    }

    /**
     * CashPosition is the last message sent by Accounting.
     * This is normally when any broker would submit its bids, so that's when
     * this Broker will do it.
     */
    public synchronized void handleMessage(TimeslotComplete tc) {
        if (tc.getTimeslotIndex() == currentTimeslot) {
            timeslotCompleted = currentTimeslot;
            notifyAll();
        } else {
            // missed a timeslot
            timeslotCompleted = timeslotRepo.currentSerialNumber();
            log.warn("Skipped timeslot " + tc.getTimeslotIndex());
        }
    }

    // The worker thread comes here to wait for the next activation
    synchronized int waitForActivation(int index) {
        try {
            int remainingTimeouts = 6; // Wait max 12 mins == 6 * maxWait
            while (running && (timeslotCompleted <= index)) {
                long maxWait = 120000 * 10;
                long nowStamp = System.currentTimeMillis();
                wait(maxWait);
                long diff = System.currentTimeMillis() - nowStamp;
                if (diff >= maxWait) {
                    if (index != 0) {
                        String msg = "worker thread waited more than " + maxWait / 1000
                                + " secs for server, abandoning game";
                        System.out.println("\n" + msg + "\n");
                        log.warn(msg);
                        running = false;
                    } else if (--remainingTimeouts <= 0) {
                        String msg = "worker thread waited more than " + "720 secs for server, abandoning game";
                        System.out.println("\n" + msg + "\n");
                        log.warn(msg);
                        running = false;
                    }
                }
            }
        } catch (InterruptedException ie) {
            log.warn("activation interrupted: " + ie);
        }
        return timeslotCompleted;
    }

    protected int getTimeslotCompleted() {
        return timeslotCompleted;
    }

    /**
     * Thread to encapsulate internal broker operations, allowing JMS threads
     * to return quickly and stay in sync with the server. 
     */
    class AgentRunner extends Thread {
        PowerTacBroker parent;
        int timeslotIndex = 0;

        public AgentRunner(PowerTacBroker parent) {
            super();
            this.parent = parent;
        }

        /**
         * In each timeslot, we must update our portfolio and trade in the 
         * wholesale market.
         */
        @Override
        public void run() {
            running = true;

            while (true) {
                timeslotIndex = waitForActivation(timeslotIndex);
                if (!running) {
                    log.info("worker thread exits at ts " + timeslotIndex);
                    return;
                }

                Timeslot current = timeslotRepo.currentTimeslot();
                log.info("activate at " + timeService.getCurrentDateTime().toString() + ", timeslot "
                        + current.getSerialNumber());
                List<Activatable> services = SpringApplicationContext.listBeansOfType(Activatable.class);
                // TODO hurts generality, improve
                movePortfolioMgrToEnd(services);
                for (Activatable svc : services) {
                    if (timeslotIndex < currentTimeslot) {
                        log.warn("broker late, ts=" + timeslotIndex + " current-ts=" + currentTimeslot);
                        break;
                    }
                    svc.activate(timeslotIndex);
                }
                if (timeslotIndex < currentTimeslot) {
                    log.warn("broker finished late, ts=" + timeslotIndex + " current-ts=" + currentTimeslot);
                }
            }
        }

        private void movePortfolioMgrToEnd(List<Activatable> services) {
            log.debug("before reordering services");
            for (Activatable svc : services) {
                log.debug(svc.getClass().getName());
            }
            Activatable portolioMgr = (Activatable) CollectionUtils.find(services, new Predicate() {
                @Override
                public boolean evaluate(Object svc) {
                    return svc.getClass() == PortfolioManagerService.class;
                }
            });
            if (portolioMgr != null) {
                services.remove(portolioMgr);
                services.add(portolioMgr);
            }
            log.debug("after reordering services");
            for (Activatable svc : services) {
                log.debug(svc.getClass().getName());
            }
        }
    }

    /**
     * Broker implementation needed to override the receiveMessage method.
     */
    class BrokerAdapter extends Broker {

        public BrokerAdapter(String username) {
            super(username);
        }

        /**
         * Here is where incoming messages actually arrive.
         */
        @Override
        public void receiveMessage(Object msg) {
            //log.info("receive " + msg.toString());
            if (msg != null) {
                // ignore all incoming messages until enabled.
                if (!(isEnabled() || msg instanceof BrokerAccept))
                    return;
                router.routeMessage(msg);
            }
        }

    }
}