blob: 2604d09213e0ba144eaae19aff6a73548ab0d128 [file] [log] [blame]
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -08001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.incubator.rpc.grpc;
17
18import static com.google.common.base.Preconditions.checkNotNull;
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -070019import static java.util.concurrent.Executors.newScheduledThreadPool;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080020import static java.util.stream.Collectors.toList;
HIGUCHI Yuta06c1a3f2016-05-23 12:54:55 -070021import static org.onosproject.incubator.protobuf.net.ProtobufUtils.translate;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080022import static org.onosproject.net.DeviceId.deviceId;
23
24import java.io.IOException;
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080025import java.util.Map;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080026import java.util.Set;
27import java.util.concurrent.CompletableFuture;
28import java.util.concurrent.ExecutionException;
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -070029import java.util.concurrent.ScheduledExecutorService;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080030import java.util.concurrent.TimeUnit;
31import java.util.concurrent.TimeoutException;
32import java.util.concurrent.atomic.AtomicInteger;
33
34import org.apache.felix.scr.annotations.Activate;
35import org.apache.felix.scr.annotations.Component;
36import org.apache.felix.scr.annotations.Deactivate;
37import org.apache.felix.scr.annotations.Modified;
38import org.apache.felix.scr.annotations.Property;
39import org.apache.felix.scr.annotations.Reference;
40import org.apache.felix.scr.annotations.ReferenceCardinality;
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -070041import org.onlab.util.Tools;
Yuta HIGUCHI9efba1e2016-07-09 11:07:13 -070042import org.onosproject.grpc.net.device.DeviceProviderRegistryRpcGrpc.DeviceProviderRegistryRpcImplBase;
HIGUCHI Yutae3e90632016-05-11 16:44:01 -070043import org.onosproject.grpc.net.device.DeviceService.DeviceConnected;
44import org.onosproject.grpc.net.device.DeviceService.DeviceDisconnected;
45import org.onosproject.grpc.net.device.DeviceService.DeviceProviderMsg;
46import org.onosproject.grpc.net.device.DeviceService.DeviceProviderServiceMsg;
47import org.onosproject.grpc.net.device.DeviceService.IsReachableResponse;
48import org.onosproject.grpc.net.device.DeviceService.PortStatusChanged;
49import org.onosproject.grpc.net.device.DeviceService.ReceivedRoleReply;
50import org.onosproject.grpc.net.device.DeviceService.RegisterProvider;
51import org.onosproject.grpc.net.device.DeviceService.UpdatePortStatistics;
52import org.onosproject.grpc.net.device.DeviceService.UpdatePorts;
HIGUCHI Yuta06c1a3f2016-05-23 12:54:55 -070053import org.onosproject.incubator.protobuf.net.ProtobufUtils;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080054import org.onosproject.net.DeviceId;
55import org.onosproject.net.MastershipRole;
Saurav Dasa2d37502016-03-25 17:50:40 -070056import org.onosproject.net.PortNumber;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080057import org.onosproject.net.device.DeviceProvider;
58import org.onosproject.net.device.DeviceProviderRegistry;
59import org.onosproject.net.device.DeviceProviderService;
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080060import org.onosproject.net.link.LinkProvider;
61import org.onosproject.net.link.LinkProviderRegistry;
62import org.onosproject.net.link.LinkProviderService;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080063import org.onosproject.net.provider.ProviderId;
64import org.osgi.service.component.ComponentContext;
65import org.slf4j.Logger;
66import org.slf4j.LoggerFactory;
67
68import com.google.common.cache.Cache;
69import com.google.common.cache.CacheBuilder;
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080070import com.google.common.collect.Maps;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080071import com.google.common.collect.Sets;
72
73import io.grpc.Server;
74import io.grpc.netty.NettyServerBuilder;
75import io.grpc.stub.StreamObserver;
76
77// gRPC Server on Metro-side
78// Translates request received on RPC channel, and calls corresponding Service on
79// Metro-ONOS cluster.
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080080
81// Currently supports DeviceProviderRegistry, LinkProviderService
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080082/**
83 * Server side implementation of gRPC based RemoteService.
84 */
85@Component(immediate = true)
86public class GrpcRemoteServiceServer {
87
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080088 static final String RPC_PROVIDER_NAME = "org.onosproject.rpc.provider.grpc";
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -080089
90 // TODO pick a number
91 public static final int DEFAULT_LISTEN_PORT = 11984;
92
93 private final Logger log = LoggerFactory.getLogger(getClass());
94
95 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
96 protected DeviceProviderRegistry deviceProviderRegistry;
97
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -080098 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
99 protected LinkProviderRegistry linkProviderRegistry;
100
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800101
102 @Property(name = "listenPort", intValue = DEFAULT_LISTEN_PORT,
103 label = "Port to listen on")
104 protected int listenPort = DEFAULT_LISTEN_PORT;
105
106 private Server server;
107 private final Set<DeviceProviderServerProxy> registeredProviders = Sets.newConcurrentHashSet();
108
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -0800109 // scheme -> ...
110 // updates must be guarded by synchronizing `this`
111 private final Map<String, LinkProviderService> linkProviderServices = Maps.newConcurrentMap();
112 private final Map<String, LinkProvider> linkProviders = Maps.newConcurrentMap();
113
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -0700114 private ScheduledExecutorService executor;
115
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800116 @Activate
117 protected void activate(ComponentContext context) throws IOException {
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -0700118 executor = newScheduledThreadPool(1, Tools.groupedThreads("grpc", "%d", log));
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800119 modified(context);
120
121 log.debug("Server starting on {}", listenPort);
122 try {
123 server = NettyServerBuilder.forPort(listenPort)
Yuta HIGUCHI9efba1e2016-07-09 11:07:13 -0700124 .addService(new DeviceProviderRegistryServerProxy())
125 .addService(new LinkProviderServiceServerProxy(this))
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800126 .build().start();
127 } catch (IOException e) {
128 log.error("Failed to start gRPC server", e);
129 throw e;
130 }
131
132 log.info("Started on {}", listenPort);
133 }
134
135 @Deactivate
136 protected void deactivate() {
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -0700137 executor.shutdown();
138 try {
139 executor.awaitTermination(5, TimeUnit.SECONDS);
140 } catch (InterruptedException e) {
141 Thread.currentThread().interrupt();
142 }
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800143
Sho SHIMIZUa09e1bb2016-08-01 14:25:25 -0700144 registeredProviders.forEach(deviceProviderRegistry::unregister);
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800145
146 server.shutdown();
147 // Should we wait for shutdown?
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -0800148
149 unregisterLinkProviders();
150
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800151 log.info("Stopped");
152 }
153
154 @Modified
155 public void modified(ComponentContext context) {
156 // TODO support dynamic reconfiguration and restarting server?
157 }
158
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -0800159 /**
160 * Registers {@link StubLinkProvider} for given ProviderId scheme.
161 *
162 * DO NOT DIRECTLY CALL THIS METHOD.
163 * Only expected to be called from {@link #getLinkProviderServiceFor(String)}.
164 *
165 * @param scheme ProviderId scheme.
166 * @return {@link LinkProviderService} registered.
167 */
168 private synchronized LinkProviderService registerStubLinkProvider(String scheme) {
169 StubLinkProvider provider = new StubLinkProvider(scheme);
170 linkProviders.put(scheme, provider);
171 return linkProviderRegistry.register(provider);
172 }
173
174 /**
175 * Unregisters all registered LinkProviders.
176 */
177 private synchronized void unregisterLinkProviders() {
HIGUCHI Yuta6381a242016-03-13 23:29:10 -0700178 // TODO remove all links registered by these providers
HIGUCHI Yuta7c1583c2015-12-03 23:08:54 -0800179 linkProviders.values().forEach(linkProviderRegistry::unregister);
180 linkProviders.clear();
181 linkProviderServices.clear();
182 }
183
184 /**
185 * Gets or creates {@link LinkProviderService} registered for given ProviderId scheme.
186 *
187 * @param scheme ProviderId scheme.
188 * @return {@link LinkProviderService}
189 */
190 protected LinkProviderService getLinkProviderServiceFor(String scheme) {
191 return linkProviderServices.computeIfAbsent(scheme, this::registerStubLinkProvider);
192 }
193
Yuta HIGUCHI4c7c90a2016-07-06 14:56:49 -0700194 protected ScheduledExecutorService getSharedExecutor() {
195 return executor;
196 }
197
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800198 // RPC Server-side code
199 // RPC session Factory
200 /**
201 * Relays DeviceProviderRegistry calls from RPC client.
202 */
Yuta HIGUCHI9efba1e2016-07-09 11:07:13 -0700203 class DeviceProviderRegistryServerProxy extends DeviceProviderRegistryRpcImplBase {
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800204
205 @Override
206 public StreamObserver<DeviceProviderServiceMsg> register(StreamObserver<DeviceProviderMsg> toDeviceProvider) {
207 log.trace("DeviceProviderRegistryServerProxy#register called!");
208
209 DeviceProviderServerProxy provider = new DeviceProviderServerProxy(toDeviceProvider);
210
211 return new DeviceProviderServiceServerProxy(provider, toDeviceProvider);
212 }
213 }
214
215 // Lower -> Upper Controller message
216 // RPC Server-side code
217 // RPC session handler
218 private final class DeviceProviderServiceServerProxy
219 implements StreamObserver<DeviceProviderServiceMsg> {
220
221 // intentionally shadowing
222 private final Logger log = LoggerFactory.getLogger(getClass());
223
224 private final DeviceProviderServerProxy pairedProvider;
225 private final StreamObserver<DeviceProviderMsg> toDeviceProvider;
226
227 private final Cache<Integer, CompletableFuture<Boolean>> outstandingIsReachable;
228
229 // wrapped providerService
230 private DeviceProviderService deviceProviderService;
231
232
233 DeviceProviderServiceServerProxy(DeviceProviderServerProxy provider,
234 StreamObserver<DeviceProviderMsg> toDeviceProvider) {
235 this.pairedProvider = provider;
236 this.toDeviceProvider = toDeviceProvider;
237 outstandingIsReachable = CacheBuilder.newBuilder()
238 .expireAfterWrite(1, TimeUnit.MINUTES)
239 .build();
240
241 // pair RPC session in other direction
242 provider.pair(this);
243 }
244
245 @Override
246 public void onNext(DeviceProviderServiceMsg msg) {
247 try {
248 log.trace("DeviceProviderServiceServerProxy received: {}", msg);
249 onMethod(msg);
250 } catch (Exception e) {
251 log.error("Exception thrown handling {}", msg, e);
252 onError(e);
253 throw e;
254 }
255 }
256
257 /**
258 * Translates received RPC message to {@link DeviceProviderService} method calls.
259 * @param msg DeviceProviderService message
260 */
261 private void onMethod(DeviceProviderServiceMsg msg) {
262 switch (msg.getMethodCase()) {
263 case REGISTER_PROVIDER:
264 RegisterProvider registerProvider = msg.getRegisterProvider();
265 // TODO Do we care about provider name?
266 pairedProvider.setProviderId(new ProviderId(registerProvider.getProviderScheme(), RPC_PROVIDER_NAME));
267 registeredProviders.add(pairedProvider);
HIGUCHI Yuta6381a242016-03-13 23:29:10 -0700268 log.info("registering DeviceProvider {} via gRPC", pairedProvider.id());
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800269 deviceProviderService = deviceProviderRegistry.register(pairedProvider);
270 break;
271
272 case DEVICE_CONNECTED:
273 DeviceConnected deviceConnected = msg.getDeviceConnected();
274 deviceProviderService.deviceConnected(deviceId(deviceConnected.getDeviceId()),
275 translate(deviceConnected.getDeviceDescription()));
276 break;
277 case DEVICE_DISCONNECTED:
278 DeviceDisconnected deviceDisconnected = msg.getDeviceDisconnected();
279 deviceProviderService.deviceDisconnected(deviceId(deviceDisconnected.getDeviceId()));
280 break;
281 case UPDATE_PORTS:
282 UpdatePorts updatePorts = msg.getUpdatePorts();
283 deviceProviderService.updatePorts(deviceId(updatePorts.getDeviceId()),
284 updatePorts.getPortDescriptionsList()
285 .stream()
HIGUCHI Yuta06c1a3f2016-05-23 12:54:55 -0700286 .map(ProtobufUtils::translate)
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800287 .collect(toList()));
288 break;
289 case PORT_STATUS_CHANGED:
290 PortStatusChanged portStatusChanged = msg.getPortStatusChanged();
291 deviceProviderService.portStatusChanged(deviceId(portStatusChanged.getDeviceId()),
292 translate(portStatusChanged.getPortDescription()));
293 break;
294 case RECEIVED_ROLE_REPLY:
295 ReceivedRoleReply receivedRoleReply = msg.getReceivedRoleReply();
296 deviceProviderService.receivedRoleReply(deviceId(receivedRoleReply.getDeviceId()),
297 translate(receivedRoleReply.getRequested()),
298 translate(receivedRoleReply.getResponse()));
299 break;
300 case UPDATE_PORT_STATISTICS:
301 UpdatePortStatistics updatePortStatistics = msg.getUpdatePortStatistics();
302 deviceProviderService.updatePortStatistics(deviceId(updatePortStatistics.getDeviceId()),
303 updatePortStatistics.getPortStatisticsList()
304 .stream()
HIGUCHI Yuta06c1a3f2016-05-23 12:54:55 -0700305 .map(ProtobufUtils::translate)
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800306 .collect(toList()));
307 break;
308
309 // return value of DeviceProvider#isReachable
310 case IS_REACHABLE_RESPONSE:
311 IsReachableResponse isReachableResponse = msg.getIsReachableResponse();
312 int xid = isReachableResponse.getXid();
313 boolean isReachable = isReachableResponse.getIsReachable();
314 CompletableFuture<Boolean> result = outstandingIsReachable.asMap().remove(xid);
315 if (result != null) {
316 result.complete(isReachable);
317 }
318 break;
319
320 case METHOD_NOT_SET:
321 default:
322 log.warn("Unexpected message received {}", msg);
323 break;
324 }
325 }
326
327 @Override
328 public void onCompleted() {
329 log.info("DeviceProviderServiceServerProxy completed");
330 deviceProviderRegistry.unregister(pairedProvider);
331 registeredProviders.remove(pairedProvider);
332 toDeviceProvider.onCompleted();
333 }
334
335 @Override
336 public void onError(Throwable e) {
337 log.error("DeviceProviderServiceServerProxy#onError", e);
HIGUCHI Yuta6381a242016-03-13 23:29:10 -0700338 if (pairedProvider != null) {
339 // TODO call deviceDisconnected against all devices
340 // registered for this provider scheme
341 log.info("unregistering DeviceProvider {} via gRPC", pairedProvider.id());
342 deviceProviderRegistry.unregister(pairedProvider);
343 registeredProviders.remove(pairedProvider);
344 }
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800345 // TODO What is the proper clean up for bi-di stream on error?
346 // sample suggests no-op
347 toDeviceProvider.onError(e);
348 }
349
350
351 /**
352 * Registers Future for {@link DeviceProvider#isReachable(DeviceId)} return value.
353 * @param xid IsReachable call ID.
354 * @param reply Future to
355 */
356 void register(int xid, CompletableFuture<Boolean> reply) {
357 outstandingIsReachable.put(xid, reply);
358 }
359
360 }
361
362 // Upper -> Lower Controller message
363 /**
364 * Relay DeviceProvider calls to RPC client.
365 */
366 private final class DeviceProviderServerProxy
367 implements DeviceProvider {
368
369 private final Logger log = LoggerFactory.getLogger(getClass());
370
371 // xid for isReachable calls
372 private final AtomicInteger xidPool = new AtomicInteger();
373 private final StreamObserver<DeviceProviderMsg> toDeviceProvider;
374
375 private DeviceProviderServiceServerProxy deviceProviderServiceProxy = null;
376 private ProviderId providerId;
377
378 DeviceProviderServerProxy(StreamObserver<DeviceProviderMsg> toDeviceProvider) {
379 this.toDeviceProvider = toDeviceProvider;
380 }
381
382 void setProviderId(ProviderId pid) {
383 this.providerId = pid;
384 }
385
386 /**
387 * Registers RPC stream in other direction.
HIGUCHI Yuta6381a242016-03-13 23:29:10 -0700388 *
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800389 * @param deviceProviderServiceProxy {@link DeviceProviderServiceServerProxy}
390 */
391 void pair(DeviceProviderServiceServerProxy deviceProviderServiceProxy) {
392 this.deviceProviderServiceProxy = deviceProviderServiceProxy;
393 }
394
395 @Override
396 public void triggerProbe(DeviceId deviceId) {
Yuta HIGUCHIad4861e2016-07-13 19:19:03 -0700397 try {
398 onTriggerProbe(deviceId);
399 } catch (Exception e) {
400 log.error("Exception caught handling triggerProbe({})",
401 deviceId, e);
402 toDeviceProvider.onError(e);
403 }
404 }
405
406 private void onTriggerProbe(DeviceId deviceId) {
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800407 log.trace("triggerProbe({})", deviceId);
408 DeviceProviderMsg.Builder msgBuilder = DeviceProviderMsg.newBuilder();
409 msgBuilder.setTriggerProbe(msgBuilder.getTriggerProbeBuilder()
410 .setDeviceId(deviceId.toString())
411 .build());
412 DeviceProviderMsg triggerProbeMsg = msgBuilder.build();
413 toDeviceProvider.onNext(triggerProbeMsg);
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800414 }
415
416 @Override
417 public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
Yuta HIGUCHIad4861e2016-07-13 19:19:03 -0700418 try {
419 onRoleChanged(deviceId, newRole);
420 } catch (Exception e) {
421 log.error("Exception caught handling onRoleChanged({}, {})",
422 deviceId, newRole, e);
423 toDeviceProvider.onError(e);
424 }
425 }
426
427 private void onRoleChanged(DeviceId deviceId, MastershipRole newRole) {
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800428 log.trace("roleChanged({}, {})", deviceId, newRole);
429 DeviceProviderMsg.Builder msgBuilder = DeviceProviderMsg.newBuilder();
430 msgBuilder.setRoleChanged(msgBuilder.getRoleChangedBuilder()
431 .setDeviceId(deviceId.toString())
432 .setNewRole(translate(newRole))
433 .build());
434 toDeviceProvider.onNext(msgBuilder.build());
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800435 }
436
437 @Override
438 public boolean isReachable(DeviceId deviceId) {
Yuta HIGUCHIad4861e2016-07-13 19:19:03 -0700439 try {
440 return onIsReachable(deviceId);
441 } catch (Exception e) {
442 log.error("Exception caught handling onIsReachable({})",
443 deviceId, e);
444 toDeviceProvider.onError(e);
445 return false;
446 }
447 }
448
449 private boolean onIsReachable(DeviceId deviceId) {
450
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800451 log.trace("isReachable({})", deviceId);
452 CompletableFuture<Boolean> result = new CompletableFuture<>();
453 final int xid = xidPool.incrementAndGet();
454
455 DeviceProviderMsg.Builder msgBuilder = DeviceProviderMsg.newBuilder();
456 msgBuilder.setIsReachableRequest(msgBuilder.getIsReachableRequestBuilder()
457 .setXid(xid)
458 .setDeviceId(deviceId.toString())
459 .build());
460
461 // Associate xid and register above future some where
462 // in DeviceProviderService channel to receive reply
463 if (deviceProviderServiceProxy != null) {
464 deviceProviderServiceProxy.register(xid, result);
465 }
466
467 // send message down RPC
468 toDeviceProvider.onNext(msgBuilder.build());
469
470 // wait for reply
471 try {
472 return result.get(10, TimeUnit.SECONDS);
473 } catch (InterruptedException e) {
474 log.debug("isReachable({}) was Interrupted", deviceId, e);
475 Thread.currentThread().interrupt();
476 } catch (TimeoutException e) {
477 log.warn("isReachable({}) Timed out", deviceId, e);
478 } catch (ExecutionException e) {
479 log.error("isReachable({}) Execution failed", deviceId, e);
Yuta HIGUCHIad4861e2016-07-13 19:19:03 -0700480 // close session
481 toDeviceProvider.onError(e);
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800482 }
483 return false;
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800484 }
485
486 @Override
487 public ProviderId id() {
488 return checkNotNull(providerId, "not initialized yet");
489 }
490
Saurav Dasa2d37502016-03-25 17:50:40 -0700491 @Override
492 public void changePortState(DeviceId deviceId, PortNumber portNumber,
493 boolean enable) {
Yuta HIGUCHIad4861e2016-07-13 19:19:03 -0700494 // TODO Implement if required
495 log.error("changePortState not supported yet");
496 toDeviceProvider.onError(new UnsupportedOperationException("not implemented yet"));
Saurav Dasa2d37502016-03-25 17:50:40 -0700497 }
498
HIGUCHI Yuta15653fd2015-11-09 11:05:09 -0800499 }
500}