Java tutorial
/* * Copyright 2011-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 org.powertac.tariffmarket; import static org.powertac.util.ListTools.*; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Instant; import org.powertac.common.Competition; import org.powertac.common.CustomerInfo; import org.powertac.common.Broker; import org.powertac.common.RandomSeed; import org.powertac.common.Rate; import org.powertac.common.Tariff; import org.powertac.common.TariffMessage; import org.powertac.common.TariffSpecification; import org.powertac.common.TariffSubscription; import org.powertac.common.TariffTransaction; import org.powertac.common.TimeService; import org.powertac.common.config.ConfigurableValue; import org.powertac.common.enumerations.PowerType; import org.powertac.common.interfaces.Accounting; import org.powertac.common.interfaces.CapacityControl; import org.powertac.common.interfaces.BrokerProxy; import org.powertac.common.interfaces.InitializationService; import org.powertac.common.interfaces.NewTariffListener; import org.powertac.common.interfaces.ServerConfiguration; import org.powertac.common.interfaces.TariffMarket; import org.powertac.common.interfaces.TimeslotPhaseProcessor; import org.powertac.common.msg.BalancingOrder; import org.powertac.common.msg.EconomicControlEvent; import org.powertac.common.msg.TariffExpire; import org.powertac.common.msg.TariffRevoke; import org.powertac.common.msg.TariffStatus; import org.powertac.common.msg.TariffUpdate; import org.powertac.common.msg.VariableRateUpdate; import org.powertac.common.repo.BrokerRepo; import org.powertac.common.repo.RandomSeedRepo; import org.powertac.common.repo.TariffRepo; import org.powertac.common.repo.TariffSubscriptionRepo; import org.powertac.common.repo.TimeslotRepo; import org.powertac.common.spring.SpringApplicationContext; import org.powertac.util.Predicate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * Implements the Tariff Market abstraction. Incoming tariff-related * messages from brokers are received and processed, tariffs are published * periodically, and subscriptions are processed on behalf of customers. * @author John Collins */ @Service public class TariffMarketService extends TimeslotPhaseProcessor implements TariffMarket, InitializationService { static private Logger log = Logger.getLogger(TariffMarketService.class.getSimpleName()); @Autowired private TimeService timeService; @Autowired private Accounting accountingService; @Autowired private CapacityControl capacityControlService; @Autowired private BrokerProxy brokerProxyService; @Autowired private BrokerRepo brokerRepo; @Autowired private TimeslotRepo timeslotRepo; @Autowired private TariffRepo tariffRepo; @Autowired private TariffSubscriptionRepo tariffSubscriptionRepo; @Autowired private ServerConfiguration serverProps; @Autowired private RandomSeedRepo randomSeedService; // list of tariffs that have been revoked but not processed private ArrayList<Tariff> pendingRevokedTariffs = new ArrayList<Tariff>(); // list of revoked but not yet deleted tariffs private List<Tariff> revokedTariffs = null; private Instant lastRevokeProcess = new Instant(0l); // configuration @ConfigurableValue(valueType = "Double", description = "low end of tariff publication fee range") private double minPublicationFee = -100.0; @ConfigurableValue(valueType = "Double", description = "high end of tariff publication fee range") private double maxPublicationFee = -500.0; @ConfigurableValue(valueType = "Double", publish = true, description = "set publication fee directly to override random selection") private Double publicationFee = null; @ConfigurableValue(valueType = "Double", description = "low end of tariff revocation fee range") private double minRevocationFee = -100.0; @ConfigurableValue(valueType = "Double", description = "high end of tariff revocation fee range") private double maxRevocationFee = -500.0; @ConfigurableValue(valueType = "Double", publish = true, description = "Set revocation fee directly to override random selection") private Double revocationFee = null; // these properties are constrained, so we provide explicit setters for them private int publicationInterval = 6; private int publicationOffset = 0; private boolean subsequentPublication; // list of pending subscription events. private List<PendingSubscription> pendingSubscriptionEvents = new ArrayList<PendingSubscription>(); // list of pending variable-rate updates. private List<VariableRateUpdate> pendingVrus = new ArrayList<VariableRateUpdate>(); // set of already-disabled brokers private HashSet<Broker> disabledBrokers = new HashSet<Broker>(); /** * Default constructor */ public TariffMarketService() { super(); } /** * Reads configuration parameters, registers for timeslot phase activation. */ @SuppressWarnings("unchecked") @Override public String initialize(Competition competition, List<String> completedInits) { int index = completedInits.indexOf("AccountingService"); if (index == -1) { return null; } for (Class<?> messageType : Arrays.asList(TariffSpecification.class, TariffExpire.class, TariffRevoke.class, VariableRateUpdate.class, EconomicControlEvent.class, BalancingOrder.class)) { brokerProxyService.registerBrokerMessageListener(this, messageType); } subsequentPublication = false; registrations.clear(); publicationFee = null; revocationFee = null; super.init(); pendingSubscriptionEvents.clear(); pendingRevokedTariffs.clear(); pendingVrus.clear(); disabledBrokers.clear(); revokedTariffs = null; lastRevokeProcess = new Instant(0l); serverProps.configureMe(this); // Register the NewTariffListeners List<NewTariffListener> listeners = SpringApplicationContext.listBeansOfType(NewTariffListener.class); for (NewTariffListener listener : listeners) { registerNewTariffListener(listener); } // compute the fees RandomSeed random = randomSeedService.getRandomSeed("TariffMarket", 0l, "fees"); if (publicationFee == null) { // interest will be non-null in case it was overridden in the config publicationFee = (minPublicationFee + (random.nextDouble() * (maxPublicationFee - minPublicationFee))); log.info("set publication fee: " + publicationFee); } if (revocationFee == null) { // interest will be non-null in case it was overridden in the config revocationFee = (minRevocationFee + (random.nextDouble() * (maxRevocationFee - minRevocationFee))); log.info("set revocation fee: " + revocationFee); } serverProps.publishConfiguration(this); return "TariffMarket"; } // ------------ Data access and configuration support --------------- // publication fee public double getMinPublicationFee() { return minPublicationFee; } public double getMaxPublicationFee() { return maxPublicationFee; } public Double getPublicationFee() { return publicationFee; } // revocation fee public double getMinRevocationFee() { return minRevocationFee; } public double getMaxRevocationFee() { return maxRevocationFee; } public Double getRevocationFee() { return revocationFee; } public int getPublicationInterval() { return publicationInterval; } @ConfigurableValue(valueType = "Integer", description = "Number of timeslots between tariff publication events. " + "Must be at most 24.") public void setPublicationInterval(int interval) { if (interval > 24) { log.error("tariff publication interval " + interval + " > 24 hr"); interval = 24; } publicationInterval = interval; } public int getPublicationOffset() { return publicationOffset; } @ConfigurableValue(valueType = "Integer", description = "Number of timeslots from the first timeslot to delay " + "the first publication event. It does not work well " + "to make this zero, because brokers do not have an opportunity " + "to post tariffs in timeslot 0.") public void setPublicationOffset(int offset) { if (offset >= publicationInterval) { log.error("tariff publication offset " + publicationOffset + " >= publication interval " + offset); } else { publicationOffset = offset; } } // Test support List<NewTariffListener> getRegistrations() { return new ArrayList<NewTariffListener>(registrations); } // ----------------- Broker message API -------------------- // Receive incoming broker messages. We do this // synchronously with the incoming message traffic, rather than on // the timeslot phase signal, to minimize latency for broker feedback. /** * Processes a newly-published tariff. */ public void handleMessage(TariffSpecification spec) { if (!(null == tariffRepo.findSpecificationById(spec.getId()) || tariffRepo.isRemoved(spec.getId()))) { log.warn("duplicate tariff spec from " + spec.getBroker().getUsername() + ", id = " + spec.getId()); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff) .withMessage("duplicate tariff spec " + spec.getId())); return; } if (null == spec.getRates()) { log.warn("no rates given for spec " + spec.getId()); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff) .withMessage("missing Rates")); return; } else if (!spec.isValid()) { log.warn("invalid spec " + spec.getId()); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff) .withMessage("spec fails validity test")); return; } else if (null != spec.getSupersedes()) { for (Long supersede : spec.getSupersedes()) { TariffSpecification other = tariffRepo.findSpecificationById(supersede); if (null == other) { log.warn("attempt to supersede non-existent tariff " + supersede); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff).withMessage("non-existent supersede " + supersede)); return; } else if (spec.getBroker() != other.getBroker()) { log.warn("attempt by " + spec.getBroker().getUsername() + " to supersede tariff of " + other.getBroker().getUsername()); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff).withMessage("invalid supersede " + supersede)); return; } } } else { for (Rate rate : spec.getRates()) { if (rate.getDailyBegin() >= 24 || rate.getDailyEnd() >= 24 || rate.getWeeklyBegin() == 0 || rate.getWeeklyBegin() > 7 || rate.getWeeklyEnd() == 0 || rate.getWeeklyEnd() > 7) { log.warn("invalid rate for spec " + spec.getId()); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff).withMessage("spec has invalid Rate")); return; } } } tariffRepo.addSpecification(spec); Tariff tariff = new Tariff(spec); if (!tariff.init()) { log.warn("incomplete coverage in multi-rate tariff " + spec.getId()); tariffRepo.removeTariff(tariff); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.invalidTariff) .withMessage("incomplete coverage in multi-rate tariff")); return; } log.info("new tariff " + spec.getId()); accountingService.addTariffTransaction(TariffTransaction.Type.PUBLISH, tariff, null, 0, 0.0, publicationFee); send(new TariffStatus(spec.getBroker(), spec.getId(), spec.getId(), TariffStatus.Status.success)); } /** * Handles changes in tariff expiration date. */ public void handleMessage(TariffExpire update) { ValidationResult result = validateUpdate(update); if (result.tariff == null) send(result.message); else { Instant newExp = update.getNewExpiration(); if (newExp != null && newExp.isBefore(timeService.getCurrentTime())) { // new expiration date in the past log.warn("attempt to set expiration for tariff " + result.tariff.getId() + " in the past:" + newExp.toString()); send(new TariffStatus(update.getBroker(), update.getTariffId(), update.getId(), TariffStatus.Status.invalidUpdate).withMessage("attempt to set expiration in the past")); } else { // update expiration date result.tariff.setExpiration(newExp); log.info("Tariff " + update.getTariffId() + "now expires at " + new DateTime(result.tariff.getExpiration(), DateTimeZone.UTC).toString()); success(update); } } } /** * Handles tariff revocation. */ public void handleMessage(TariffRevoke update) { // basic validation ValidationResult result = validateUpdate(update); if (result.tariff == null) { send(result.message); return; } addPendingRevoke(result.tariff); success(update); } /** * Applies a new HourlyCharge to an existing Tariff with a variable Rate. */ public void handleMessage(VariableRateUpdate update) { ValidationResult result = validateUpdate(update); if (result.tariff == null) { send(result.message); return; } Rate rate = tariffRepo.findRateById(update.getRateId()); if (rate == null) { send(new TariffStatus(update.getBroker(), update.getTariffId(), update.getId(), TariffStatus.Status.invalidUpdate).withMessage("Non-existent rate in VRU")); return; } if (!result.tariff.getTariffSpecification().getRates().contains(rate)) { send(new TariffStatus(update.getBroker(), update.getTariffId(), update.getId(), TariffStatus.Status.invalidUpdate).withMessage("Rate not associated with tariff in VRU")); return; } if (!update.isValid(rate)) { send(new TariffStatus(update.getBroker(), update.getTariffId(), update.getId(), TariffStatus.Status.invalidUpdate).withMessage("Invalid charge in VRU")); return; } addVru(update); } /** * Processes an incoming ControlEvent from a broker */ public synchronized void handleMessage(EconomicControlEvent msg) { ValidationResult result = validateUpdate(msg); if (result.tariff == null) { send(result.message); return; } int currentTimeslot = timeslotRepo.currentTimeslot().getSerialNumber(); if (currentTimeslot > msg.getTimeslotIndex()) { // this is in the past log.warn("Curtailment requested in ts " + currentTimeslot + " for past timeslot " + msg.getTimeslotIndex()); // send error? send(new TariffStatus(msg.getBroker(), msg.getTariffId(), msg.getId(), TariffStatus.Status.invalidUpdate).withMessage("control: specified timeslot in the past")); return; } capacityControlService.postEconomicControl(msg); } /** * Processes an incoming BalancingOrder by storing it in the tariffRepo */ public synchronized void handleMessage(BalancingOrder msg) { ValidationResult result = validateUpdate(msg); if (result.tariff == null) { send(result.message); return; } tariffRepo.addBalancingOrder(msg); } // ----------------------- Customer API -------------------------- private synchronized void addPendingRevoke(Tariff tariff) { pendingRevokedTariffs.add(tariff); } private synchronized List<Tariff> getPendingRevokes() { Instant now = timeService.getCurrentTime(); if (now.isAfter(lastRevokeProcess)) { lastRevokeProcess = now; List<Tariff> result = new ArrayList<Tariff>(pendingRevokedTariffs); pendingRevokedTariffs.clear(); return result; } return null; // only get non-null result once/timeslot } /** * Runs through the list of pending tariff revocations, marking the tariffs * and their subscriptions. */ @Override public void processRevokedTariffs() { // nothing happens -- this is deprecated. } private void revokeTariffsForDisabledBrokers() { for (Broker broker : brokerRepo.findDisabledBrokers()) { if (!disabledBrokers.contains(broker)) { // this is a new one disabledBrokers.add(broker); for (Tariff tariff : tariffRepo.findTariffsByBroker(broker)) { if (Tariff.State.KILLED != tariff.getState()) { log.info("Revoking tariff " + tariff.getId() + " from disabled broker " + broker.getUsername()); addPendingRevoke(tariff); } } } } } private void updateRevokedTariffs() { List<Tariff> pending = getPendingRevokes(); if (pending == null) return; revokedTariffs = pending; for (Tariff tariff : pending) { tariff.setState(Tariff.State.KILLED); log.info("Revoke tariff " + tariff.getId()); // Notify all brokers - issue #719 TariffRevoke msg = new TariffRevoke(tariff.getBroker(), tariff.getTariffSpecification()); brokerProxyService.broadcastMessage(msg); // The actual revocation processing is delegated to the Customer, // who is obligated to call getRevokedSubscriptions periodically. // If there are active subscriptions, then we have to charge a fee. List<TariffSubscription> activeSubscriptions = filter( tariffSubscriptionRepo.findSubscriptionsForTariff(tariff), new Predicate<TariffSubscription>() { @Override public boolean apply(TariffSubscription sub) { return (sub.getCustomersCommitted() > 0); } }); if (activeSubscriptions.size() > 0) { log.info("Revoked tariff " + tariff.getId() + " has " + activeSubscriptions.size() + " active subscriptions"); accountingService.addTariffTransaction(TariffTransaction.Type.REVOKE, tariff, null, 0, 0.0, revocationFee); } } } // Removes revoked tariffs and their subscriptions from their respective // repos. void removeRevokedTariffs() { if (null == revokedTariffs) return; for (Tariff tariff : revokedTariffs) { // remove all subscriptions tariffSubscriptionRepo.removeSubscriptionsForTariff(tariff); // then remove the tariff and the tariffSpec tariffRepo.removeTariff(tariff); } revokedTariffs = null; } private Set<NewTariffListener> registrations = new HashSet<NewTariffListener>(); @Override public void registerNewTariffListener(NewTariffListener listener) { registrations.add(listener); } // Process queued messages, then // handle distribution of new tariffs to customers @Override public void activate(Instant time, int phase) { log.info("Activate"); processPendingVrus(); long msec = timeService.getCurrentTime().getMillis(); if (!subsequentPublication || (msec / TimeService.HOUR) % publicationInterval == publicationOffset) { // time to publish or never published revokeTariffsForDisabledBrokers(); updateRevokedTariffs(); publishTariffs(); //removeRevokedTariffs(); processPendingSubscriptions(); subsequentPublication = true; } } /** * Publishes pending tariffs to customers and brokers */ private void publishTariffs() { List<Tariff> publishedTariffs = tariffRepo.findTariffsByState(Tariff.State.PENDING); log.info("publishing " + publishedTariffs.size() + " new tariffs"); for (Tariff tariff : publishedTariffs) { tariff.setState(Tariff.State.OFFERED); } List<TariffSpecification> publishedTariffSpecs = new ArrayList<TariffSpecification>(); for (Tariff tariff : publishedTariffs) { TariffSpecification spec = tariff.getTariffSpecification(); publishedTariffSpecs.add(spec); log.info("publishing spec " + spec.getId() + " broker: " + spec.getBroker().getUsername() + ", exp: " + spec.getExpiration()); } for (NewTariffListener listener : registrations) { listener.publishNewTariffs(publishedTariffs); } brokerProxyService.broadcastMessages(publishedTariffSpecs); } @Override public List<Tariff> getActiveTariffList(PowerType type) { return tariffRepo.findActiveTariffs(type); } /** * Returns the default tariff */ @Override public Tariff getDefaultTariff(PowerType type) { return tariffRepo.getDefaultTariff(type); } @Override public boolean setDefaultTariff(TariffSpecification newSpec) { tariffRepo.setDefaultTariff(newSpec); return true; } // ----------- Subscribe/unsubscribe processing -------------- /** * If customerCount is positive, subscribes a block of Customers * from a single Customer model to the specified Tariff, as long * as the Tariff is not expired or revoked. If customerCount is negative, * unsubscribes a block of customers from the specified tariff. * Processing is deferred unless the customer has no subscriptions, * which should only be true at the start of a boot or sim session. * <p> * Note that you cannot unsubscribe directly from a Tariff -- you have to do * that from the TariffSubscription that represents the Tariff you want * to unsubscribe from.</p> */ @Override public void subscribeToTariff(Tariff tariff, CustomerInfo customer, int customerCount) { if (customerCount < 0 || !(tariff.isExpired() || tariff.isRevoked())) { postPendingSubscriptionEvent(tariff, customer, customerCount); List<TariffSubscription> existingSubscriptions = tariffSubscriptionRepo .findSubscriptionsForCustomer(customer); if (0 == existingSubscriptions.size()) { // immediate processing of initial subscriptions processPendingSubscriptions(); } } else log.warn("Attempt to subscribe to " + (tariff.isRevoked() ? "revoked" : "expired") + " tariff"); } /** * Adds a pending subscribe/unsubscribe for later processing */ private synchronized void postPendingSubscriptionEvent(Tariff tariff, CustomerInfo customer, int customerCount) { PendingSubscription event = new PendingSubscription(tariff, customer, customerCount); pendingSubscriptionEvents.add(event); } /** * Handles pending subscription/unsubscription events */ private synchronized void processPendingSubscriptions() { for (PendingSubscription pending : pendingSubscriptionEvents) { TariffSubscription sub = tariffSubscriptionRepo.getSubscription(pending.customer, pending.tariff); if (pending.count > 0) sub.subscribe(pending.count); else if (pending.count < 0) sub.deferredUnsubscribe(-pending.count); } pendingSubscriptionEvents.clear(); } /** * Handles pending vru messages */ private void processPendingVrus() { for (VariableRateUpdate vru : getVruList()) { Tariff tariff = tariffRepo.findTariffById(vru.getTariffId()); if (tariff.addHourlyCharge(vru.getHourlyCharge(), vru.getRateId())) { success(vru); } else { // failed to add hourly charge send(new TariffStatus(vru.getBroker(), vru.getTariffId(), vru.getId(), TariffStatus.Status.invalidUpdate).withMessage("update: could not add hourly charge")); } } } // adds a VariableRateUpdate to the shared list private synchronized void addVru(VariableRateUpdate newVru) { pendingVrus.add(newVru); } // transfers the contents of the pending VRU list to the caller private synchronized List<VariableRateUpdate> getVruList() { ArrayList<VariableRateUpdate> result = new ArrayList<VariableRateUpdate>(pendingVrus); pendingVrus.clear(); return result; } // ------------------ Helper stuff --------------- private void success(TariffUpdate update) { Broker broker = update.getBroker(); send(new TariffStatus(broker, update.getTariffId(), update.getId(), TariffStatus.Status.success)); } // sends a message to the broker private void send(TariffMessage msg) { if (null == msg) { log.debug("null outgoing message"); } else { brokerProxyService.sendMessage(msg.getBroker(), msg); } } private ValidationResult validateUpdate(TariffUpdate update) { Broker broker = update.getBroker(); Tariff tariff = tariffRepo.findTariffById(update.getTariffId()); if (tariff == null) { log.error("update - no such tariff " + update.getTariffId() + ", broker " + update.getBroker().getUsername()); return new ValidationResult(null, new TariffStatus(broker, update.getTariffId(), update.getId(), TariffStatus.Status.noSuchTariff)); } if (broker != tariff.getBroker()) { log.error("update - attempt by " + broker.getUsername() + " to revoke " + tariff.getBroker() + "'s tariff"); return new ValidationResult(null, new TariffStatus(broker, update.getTariffId(), update.getId(), TariffStatus.Status.invalidTariff)); } return new ValidationResult(tariff, null); } // data structure for message validation result private class ValidationResult { Tariff tariff; TariffStatus message; ValidationResult(Tariff tariff, TariffStatus msg) { super(); this.tariff = tariff; this.message = msg; } } // data structure for pending subscription events private class PendingSubscription { Tariff tariff; CustomerInfo customer; int count; PendingSubscription(Tariff tariff, CustomerInfo customer, int count) { super(); this.tariff = tariff; this.customer = customer; this.count = count; } } @Override public void setDefaults() { } // @Override // public void // registerNewSubscriptionRepoListener (SubscriptionRepoListener listener) // { // subscriptionRepoRegistrations.add(listener); // } }