cbac02cc9675f972cf60b41ea42f4c880c055d24
[controller.git] / opendaylight / md-sal / samples / toaster-provider / src / main / java / org / opendaylight / controller / sample / toaster / provider / OpendaylightToaster.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.sample.toaster.provider;
9
10 import static org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType.DELETE;
11 import static org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType.WRITE;
12 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.CONFIGURATION;
13 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL;
14 import static org.opendaylight.yangtools.yang.common.RpcError.ErrorType.APPLICATION;
15
16 import com.google.common.base.Function;
17 import com.google.common.base.Optional;
18 import com.google.common.base.Preconditions;
19 import com.google.common.util.concurrent.FutureCallback;
20 import com.google.common.util.concurrent.Futures;
21 import com.google.common.util.concurrent.ListenableFuture;
22 import com.google.common.util.concurrent.MoreExecutors;
23 import com.google.common.util.concurrent.SettableFuture;
24 import java.util.Collection;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.atomic.AtomicLong;
30 import java.util.concurrent.atomic.AtomicReference;
31 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
32 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
33 import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener;
34 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
35 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
36 import org.opendaylight.controller.md.sal.binding.api.NotificationPublishService;
37 import org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction;
38 import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
39 import org.opendaylight.controller.md.sal.common.api.data.OptimisticLockFailedException;
40 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
41 import org.opendaylight.controller.md.sal.common.util.jmx.AbstractMXBean;
42 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.CancelToastInput;
43 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.CancelToastOutput;
44 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.CancelToastOutputBuilder;
45 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.DisplayString;
46 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.MakeToastInput;
47 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.MakeToastOutput;
48 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.MakeToastOutputBuilder;
49 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.RestockToasterInput;
50 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.RestockToasterOutput;
51 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.RestockToasterOutputBuilder;
52 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.Toaster;
53 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.Toaster.ToasterStatus;
54 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterBuilder;
55 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterOutOfBreadBuilder;
56 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterRestocked;
57 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterRestockedBuilder;
58 import org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterService;
59 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.toaster.app.config.rev160503.ToasterAppConfig;
60 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.toaster.app.config.rev160503.ToasterAppConfigBuilder;
61 import org.opendaylight.yangtools.concepts.ListenerRegistration;
62 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
63 import org.opendaylight.yangtools.yang.common.RpcError;
64 import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
65 import org.opendaylight.yangtools.yang.common.RpcResult;
66 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 public class OpendaylightToaster extends AbstractMXBean
71         implements ToasterService, ToasterProviderRuntimeMXBean, DataTreeChangeListener<Toaster>, AutoCloseable {
72
73     private static final CancelToastOutput EMPTY_CANCEL_OUTPUT = new CancelToastOutputBuilder().build();
74     private static final MakeToastOutput EMPTY_MAKE_OUTPUT = new MakeToastOutputBuilder().build();
75     private static final RestockToasterOutput EMPTY_RESTOCK_OUTPUT = new RestockToasterOutputBuilder().build();
76
77     private static final Logger LOG = LoggerFactory.getLogger(OpendaylightToaster.class);
78
79     private static final InstanceIdentifier<Toaster> TOASTER_IID = InstanceIdentifier.builder(Toaster.class).build();
80     private static final DisplayString TOASTER_MANUFACTURER = new DisplayString("Opendaylight");
81     private static final DisplayString TOASTER_MODEL_NUMBER = new DisplayString("Model 1 - Binding Aware");
82
83     private DataBroker dataBroker;
84     private NotificationPublishService notificationProvider;
85     private ListenerRegistration<OpendaylightToaster> dataTreeChangeListenerRegistration;
86
87     private final ExecutorService executor;
88
89     // This holds the Future for the current make toast task and is used to cancel the current toast.
90     private final AtomicReference<Future<?>> currentMakeToastTask = new AtomicReference<>();
91
92     // Thread safe holders
93     private final AtomicLong amountOfBreadInStock = new AtomicLong(100);
94     private final AtomicLong toastsMade = new AtomicLong(0);
95     private final AtomicLong darknessFactor = new AtomicLong(1000);
96
97     private final ToasterAppConfig toasterAppConfig;
98
99     public OpendaylightToaster() {
100         this(new ToasterAppConfigBuilder().setManufacturer(TOASTER_MANUFACTURER).setModelNumber(TOASTER_MODEL_NUMBER)
101                 .setMaxMakeToastTries(2).build());
102     }
103
104     public OpendaylightToaster(final ToasterAppConfig toasterAppConfig) {
105         super("OpendaylightToaster", "toaster-provider", null);
106         executor = Executors.newFixedThreadPool(1);
107         this.toasterAppConfig = toasterAppConfig;
108     }
109
110     public void setNotificationProvider(final NotificationPublishService notificationPublishService) {
111         this.notificationProvider = notificationPublishService;
112     }
113
114     public void setDataBroker(final DataBroker dataBroker) {
115         this.dataBroker = dataBroker;
116     }
117
118     public void init() {
119         LOG.info("Initializing...");
120
121         Preconditions.checkNotNull(dataBroker, "dataBroker must be set");
122         dataTreeChangeListenerRegistration = dataBroker.registerDataTreeChangeListener(
123                 new DataTreeIdentifier<>(CONFIGURATION, TOASTER_IID), this);
124         setToasterStatusUp(null);
125
126         // Register our MXBean.
127         register();
128     }
129
130     /**
131      * Implemented from the AutoCloseable interface.
132      */
133     @Override
134     public void close() {
135         LOG.info("Closing...");
136
137         // Unregister our MXBean.
138         unregister();
139
140         // When we close this service we need to shutdown our executor!
141         executor.shutdown();
142
143         if (dataTreeChangeListenerRegistration != null) {
144             dataTreeChangeListenerRegistration.close();
145         }
146
147         if (dataBroker != null) {
148             WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
149             tx.delete(OPERATIONAL,TOASTER_IID);
150             Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
151                 @Override
152                 public void onSuccess(final Void result) {
153                     LOG.debug("Successfully deleted the operational Toaster");
154                 }
155
156                 @Override
157                 public void onFailure(final Throwable failure) {
158                     LOG.error("Delete of the operational Toaster failed", failure);
159                 }
160             }, MoreExecutors.directExecutor());
161         }
162     }
163
164     private Toaster buildToaster(final ToasterStatus status) {
165         // note - we are simulating a device whose manufacture and model are
166         // fixed (embedded) into the hardware.
167         // This is why the manufacture and model number are hardcoded.
168         return new ToasterBuilder().setToasterManufacturer(toasterAppConfig.getManufacturer())
169                 .setToasterModelNumber(toasterAppConfig.getModelNumber()).setToasterStatus(status).build();
170     }
171
172     /**
173      * Implemented from the DataTreeChangeListener interface.
174      */
175     @Override
176     public void onDataTreeChanged(final Collection<DataTreeModification<Toaster>> changes) {
177         for (DataTreeModification<Toaster> change: changes) {
178             DataObjectModification<Toaster> rootNode = change.getRootNode();
179             if (rootNode.getModificationType() == WRITE) {
180                 Toaster oldToaster = rootNode.getDataBefore();
181                 Toaster newToaster = rootNode.getDataAfter();
182                 LOG.info("onDataTreeChanged - Toaster config with path {} was added or replaced: "
183                         + "old Toaster: {}, new Toaster: {}", change.getRootPath().getRootIdentifier(),
184                         oldToaster, newToaster);
185
186                 Long darkness = newToaster.getDarknessFactor();
187                 if (darkness != null) {
188                     darknessFactor.set(darkness);
189                 }
190             } else if (rootNode.getModificationType() == DELETE) {
191                 LOG.info("onDataTreeChanged - Toaster config with path {} was deleted: old Toaster: {}",
192                         change.getRootPath().getRootIdentifier(), rootNode.getDataBefore());
193             }
194         }
195     }
196
197     /**
198      * RPC call implemented from the ToasterService interface that cancels the current toast, if any.
199      */
200     @Override
201     public ListenableFuture<RpcResult<CancelToastOutput>> cancelToast(final CancelToastInput input) {
202         Future<?> current = currentMakeToastTask.getAndSet(null);
203         if (current != null) {
204             current.cancel(true);
205         }
206
207         // Always return success from the cancel toast call
208         return Futures.immediateFuture(RpcResultBuilder.success(EMPTY_CANCEL_OUTPUT).build());
209     }
210
211     /**
212      * RPC call implemented from the ToasterService interface that attempts to make toast.
213      */
214     @Override
215     public ListenableFuture<RpcResult<MakeToastOutput>> makeToast(final MakeToastInput input) {
216         LOG.info("makeToast: " + input);
217
218         final SettableFuture<RpcResult<MakeToastOutput>> futureResult = SettableFuture.create();
219
220         checkStatusAndMakeToast(input, futureResult, toasterAppConfig.getMaxMakeToastTries());
221
222         return futureResult;
223     }
224
225     private static RpcError makeToasterOutOfBreadError() {
226         return RpcResultBuilder.newError(APPLICATION, "resource-denied", "Toaster is out of bread", "out-of-stock",
227                 null, null);
228     }
229
230     private static RpcError makeToasterInUseError() {
231         return RpcResultBuilder.newWarning(APPLICATION, "in-use", "Toaster is busy", null, null, null);
232     }
233
234     private void checkStatusAndMakeToast(final MakeToastInput input,
235             final SettableFuture<RpcResult<MakeToastOutput>> futureResult, final int tries) {
236         // Read the ToasterStatus and, if currently Up, try to write the status to Down.
237         // If that succeeds, then we essentially have an exclusive lock and can proceed
238         // to make toast.
239         final ReadWriteTransaction tx = dataBroker.newReadWriteTransaction();
240         ListenableFuture<Optional<Toaster>> readFuture = tx.read(OPERATIONAL, TOASTER_IID);
241
242         final ListenableFuture<Void> commitFuture =
243             Futures.transformAsync(readFuture, toasterData -> {
244                 ToasterStatus toasterStatus = ToasterStatus.Up;
245                 if (toasterData.isPresent()) {
246                     toasterStatus = toasterData.get().getToasterStatus();
247                 }
248
249                 LOG.debug("Read toaster status: {}", toasterStatus);
250
251                 if (toasterStatus == ToasterStatus.Up) {
252
253                     if (outOfBread()) {
254                         LOG.debug("Toaster is out of bread");
255                         return Futures.immediateFailedCheckedFuture(
256                                 new TransactionCommitFailedException("", makeToasterOutOfBreadError()));
257                     }
258
259                     LOG.debug("Setting Toaster status to Down");
260
261                     // We're not currently making toast - try to update the status to Down
262                     // to indicate we're going to make toast. This acts as a lock to prevent
263                     // concurrent toasting.
264                     tx.put(OPERATIONAL, TOASTER_IID, buildToaster(ToasterStatus.Down));
265                     return tx.submit();
266                 }
267
268                 LOG.debug("Oops - already making toast!");
269
270                 // Return an error since we are already making toast. This will get
271                 // propagated to the commitFuture below which will interpret the null
272                 // TransactionStatus in the RpcResult as an error condition.
273                 return Futures.immediateFailedCheckedFuture(
274                         new TransactionCommitFailedException("", makeToasterInUseError()));
275             }, MoreExecutors.directExecutor());
276
277         Futures.addCallback(commitFuture, new FutureCallback<Void>() {
278             @Override
279             public void onSuccess(final Void result) {
280                 // OK to make toast
281                 currentMakeToastTask.set(executor.submit(new MakeToastTask(input, futureResult)));
282             }
283
284             @Override
285             public void onFailure(final Throwable ex) {
286                 if (ex instanceof OptimisticLockFailedException) {
287
288                     // Another thread is likely trying to make toast simultaneously and updated the
289                     // status before us. Try reading the status again - if another make toast is
290                     // now in progress, we should get ToasterStatus.Down and fail.
291
292                     if (tries - 1 > 0) {
293                         LOG.debug("Got OptimisticLockFailedException - trying again");
294                         checkStatusAndMakeToast(input, futureResult, tries - 1);
295                     } else {
296                         futureResult.set(RpcResultBuilder.<MakeToastOutput>failed()
297                                 .withError(ErrorType.APPLICATION, ex.getMessage()).build());
298                     }
299                 } else if (ex instanceof TransactionCommitFailedException) {
300                     LOG.debug("Failed to commit Toaster status", ex);
301
302                     // Probably already making toast.
303                     futureResult.set(RpcResultBuilder.<MakeToastOutput>failed()
304                             .withRpcErrors(((TransactionCommitFailedException)ex).getErrorList()).build());
305                 } else {
306                     LOG.debug("Unexpected error committing Toaster status", ex);
307                     futureResult.set(RpcResultBuilder.<MakeToastOutput>failed().withError(ErrorType.APPLICATION,
308                             "Unexpected error committing Toaster status", ex).build());
309                 }
310             }
311         }, MoreExecutors.directExecutor());
312     }
313
314     /**
315      * RestConf RPC call implemented from the ToasterService interface.
316      * Restocks the bread for the toaster, resets the toastsMade counter to 0, and sends a
317      * ToasterRestocked notification.
318      */
319     @Override
320     public ListenableFuture<RpcResult<RestockToasterOutput>> restockToaster(final RestockToasterInput input) {
321         LOG.info("restockToaster: " + input);
322
323         amountOfBreadInStock.set(input.getAmountOfBreadToStock());
324
325         if (amountOfBreadInStock.get() > 0) {
326             ToasterRestocked reStockedNotification = new ToasterRestockedBuilder()
327                     .setAmountOfBread(input.getAmountOfBreadToStock()).build();
328             notificationProvider.offerNotification(reStockedNotification);
329         }
330
331         return Futures.immediateFuture(RpcResultBuilder.success(EMPTY_RESTOCK_OUTPUT).build());
332     }
333
334     /**
335      * JMX RPC call implemented from the ToasterProviderRuntimeMXBean interface.
336      */
337     @Override
338     public void clearToastsMade() {
339         LOG.info("clearToastsMade");
340         toastsMade.set(0);
341     }
342
343     /**
344      * Accesssor method implemented from the ToasterProviderRuntimeMXBean interface.
345      */
346     @Override
347     public Long getToastsMade() {
348         return toastsMade.get();
349     }
350
351     private void setToasterStatusUp(final Function<Boolean, MakeToastOutput> resultCallback) {
352         WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
353         tx.put(OPERATIONAL,TOASTER_IID, buildToaster(ToasterStatus.Up));
354
355         Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
356             @Override
357             public void onSuccess(final Void result) {
358                 LOG.info("Successfully set ToasterStatus to Up");
359                 notifyCallback(true);
360             }
361
362             @Override
363             public void onFailure(final Throwable failure) {
364                 // We shouldn't get an OptimisticLockFailedException (or any ex) as no
365                 // other component should be updating the operational state.
366                 LOG.error("Failed to update toaster status", failure);
367
368                 notifyCallback(false);
369             }
370
371             void notifyCallback(final boolean result) {
372                 if (resultCallback != null) {
373                     resultCallback.apply(result);
374                 }
375             }
376         }, MoreExecutors.directExecutor());
377     }
378
379     private boolean outOfBread() {
380         return amountOfBreadInStock.get() == 0;
381     }
382
383     private class MakeToastTask implements Callable<Void> {
384
385         final MakeToastInput toastRequest;
386         final SettableFuture<RpcResult<MakeToastOutput>> futureResult;
387
388         MakeToastTask(final MakeToastInput toastRequest,
389             final SettableFuture<RpcResult<MakeToastOutput>> futureResult) {
390             this.toastRequest = toastRequest;
391             this.futureResult = futureResult;
392         }
393
394         @Override
395         public Void call() {
396             try {
397                 // make toast just sleeps for n seconds per doneness level.
398                 Thread.sleep(OpendaylightToaster.this.darknessFactor.get() * toastRequest.getToasterDoneness());
399
400             } catch (InterruptedException e) {
401                 LOG.info("Interrupted while making the toast");
402             }
403
404             toastsMade.incrementAndGet();
405
406             amountOfBreadInStock.getAndDecrement();
407             if (outOfBread()) {
408                 LOG.info("Toaster is out of bread!");
409
410                 notificationProvider.offerNotification(new ToasterOutOfBreadBuilder().build());
411             }
412
413             // Set the Toaster status back to up - this essentially releases the toasting lock.
414             // We can't clear the current toast task nor set the Future result until the
415             // update has been committed so we pass a callback to be notified on completion.
416
417             setToasterStatusUp(result -> {
418                 currentMakeToastTask.set(null);
419                 LOG.debug("Toast done");
420                 futureResult.set(RpcResultBuilder.success(EMPTY_MAKE_OUTPUT).build());
421                 return null;
422             });
423
424             return null;
425         }
426     }
427 }