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