001/** 002 * Copyright 2005-2017 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.ksb.impl.bus; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Random; 026import java.util.Set; 027import java.util.concurrent.ScheduledFuture; 028import java.util.concurrent.TimeUnit; 029 030import javax.xml.namespace.QName; 031 032import org.apache.commons.lang.StringUtils; 033import org.apache.log4j.Logger; 034import org.kuali.rice.core.api.config.property.Config; 035import org.kuali.rice.core.api.config.property.ConfigContext; 036import org.kuali.rice.core.api.lifecycle.BaseLifecycle; 037import org.kuali.rice.ksb.api.bus.Endpoint; 038import org.kuali.rice.ksb.api.bus.ServiceBus; 039import org.kuali.rice.ksb.api.bus.ServiceConfiguration; 040import org.kuali.rice.ksb.api.bus.ServiceDefinition; 041import org.kuali.rice.ksb.api.registry.RemoveAndPublishResult; 042import org.kuali.rice.ksb.api.registry.ServiceEndpoint; 043import org.kuali.rice.ksb.api.registry.ServiceInfo; 044import org.kuali.rice.ksb.api.registry.ServiceRegistry; 045import org.kuali.rice.ksb.impl.bus.diff.CompleteServiceDiff; 046import org.kuali.rice.ksb.impl.bus.diff.LocalServicesDiff; 047import org.kuali.rice.ksb.impl.bus.diff.RemoteServicesDiff; 048import org.kuali.rice.ksb.impl.bus.diff.ServiceRegistryDiffCalculator; 049import org.kuali.rice.ksb.messaging.serviceexporters.ServiceExportManager; 050import org.kuali.rice.ksb.messaging.threadpool.KSBScheduledPool; 051import org.springframework.beans.factory.DisposableBean; 052import org.springframework.beans.factory.InitializingBean; 053 054public class ServiceBusImpl extends BaseLifecycle implements ServiceBus, InitializingBean, DisposableBean { 055 056 private static final Logger LOG = Logger.getLogger(ServiceBusImpl.class); 057 058 private final Object serviceLock = new Object(); 059 private final Object synchronizeLock = new Object(); 060 private final Random randomNumber = new Random(); 061 062 // injected values 063 private String instanceId; 064 private ServiceRegistry serviceRegistry; 065 private ServiceRegistryDiffCalculator diffCalculator; 066 private ServiceExportManager serviceExportManager; 067 private KSBScheduledPool scheduledPool; 068 069 private ScheduledFuture<?> registrySyncFuture; 070 071 /** 072 * Contains endpoints for services which were published by this client application. 073 */ 074 private final Map<QName, LocalService> localServices; 075 076 /** 077 * Contains endpoints for services which exist remotely. This list may not be 078 * entirely complete as entries get lazily loaded into it as services are requested. 079 */ 080 private final Map<QName, Set<RemoteService>> clientRegistryCache; 081 082 public ServiceBusImpl() { 083 this.localServices = new HashMap<QName, LocalService>(); 084 this.clientRegistryCache = new HashMap<QName, Set<RemoteService>>(); 085 } 086 087 @Override 088 public void afterPropertiesSet() throws Exception { 089 if (StringUtils.isBlank(instanceId)) { 090 throw new IllegalStateException("a valid instanceId was not injected"); 091 } 092 if (serviceRegistry == null) { 093 throw new IllegalStateException("serviceRegistry was not injected"); 094 } 095 if (diffCalculator == null) { 096 throw new IllegalStateException("diffCalculator was not injected"); 097 } 098 if (scheduledPool == null) { 099 throw new IllegalStateException("scheduledPool was not injected"); 100 } 101 } 102 103 @Override 104 public void start() throws Exception { 105 startSynchronizationThread(); 106 super.start(); 107 } 108 109 protected boolean isDevMode() { 110 return ConfigContext.getCurrentContextConfig().getDevMode(); 111 } 112 113 protected void startSynchronizationThread() { 114 synchronized (synchronizeLock) { 115 LOG.info("Starting Service Bus synchronization thread..."); 116 if (!isDevMode()) { 117 int refreshRate = ConfigContext.getCurrentContextConfig().getRefreshRate(); 118 Runnable runnable = new Runnable() { 119 public void run() { 120 try { 121 synchronize(); 122 } catch (Throwable t) { 123 LOG.error("Failed to execute background service bus synchronization.", t); 124 } 125 } 126 }; 127 this.registrySyncFuture = scheduledPool.scheduleWithFixedDelay(runnable, 30, refreshRate, TimeUnit.SECONDS); 128 } 129 LOG.info("...Service Bus synchronization thread successfully started."); 130 } 131 } 132 133 @Override 134 public void destroy() throws Exception { 135 LOG.info("Stopping the Service Bus..."); 136 stopSynchronizationThread(); 137 serviceRegistry.takeInstanceOffline(getInstanceId()); 138 LOG.info("...Service Bus successfully stopped."); 139 } 140 141 protected void stopSynchronizationThread() { 142 synchronized (synchronizeLock) { 143 // remove services from the bus 144 if (this.registrySyncFuture != null) { 145 if (!this.registrySyncFuture.cancel(false)) { 146 LOG.warn("Failed to cancel registry sychronization."); 147 } 148 this.registrySyncFuture = null; 149 } 150 } 151 } 152 153 @Override 154 public String getInstanceId() { 155 return this.instanceId; 156 } 157 158 public void setInstanceId(String instanceId) { 159 this.instanceId = instanceId; 160 } 161 162 @Override 163 public List<Endpoint> getEndpoints(QName serviceName) { 164 return getEndpoints(serviceName, null); 165 } 166 167 @Override 168 public List<Endpoint> getEndpoints(QName serviceName, String applicationId) { 169 if (serviceName == null) { 170 throw new IllegalArgumentException("serviceName cannot be null"); 171 } 172 List<Endpoint> endpoints = new ArrayList<Endpoint>(); 173 synchronized (serviceLock) { 174 endpoints.addAll(getRemoteEndpoints(serviceName)); 175 Endpoint localEndpoint = getLocalEndpoint(serviceName); 176 if (localEndpoint != null) { 177 for (Iterator<Endpoint> iterator = endpoints.iterator(); iterator.hasNext();) { 178 Endpoint endpoint = iterator.next(); 179 if (localEndpoint.getServiceConfiguration().equals(endpoint.getServiceConfiguration())) { 180 iterator.remove(); 181 break; 182 } 183 } 184 if(StringUtils.isBlank(applicationId) || StringUtils.equals(localEndpoint.getServiceConfiguration().getApplicationId(), applicationId)) { 185 // add at first position, just because we like the local endpoint the best, it's our friend ;) 186 endpoints.add(0, localEndpoint); 187 } 188 } 189 if(StringUtils.isNotBlank(applicationId)) { 190 for (Iterator<Endpoint> iterator = endpoints.iterator(); iterator.hasNext();) { 191 Endpoint endpoint = (Endpoint) iterator.next(); 192 if(!StringUtils.equals(endpoint.getServiceConfiguration().getApplicationId(), applicationId)) { 193 iterator.remove(); 194 } 195 } 196 } 197 } 198 return Collections.unmodifiableList(endpoints); 199 } 200 201 @Override 202 public List<Endpoint> getRemoteEndpoints(QName serviceName) { 203 if (serviceName == null) { 204 throw new IllegalArgumentException("serviceName cannot be null"); 205 } 206 List<Endpoint> endpoints = new ArrayList<Endpoint>(); 207 synchronized (serviceLock) { 208 Set<RemoteService> remoteServices = clientRegistryCache.get(serviceName); 209 if (remoteServices != null) { 210 for (RemoteService remoteService : remoteServices) { 211 endpoints.add(remoteService.getEndpoint()); 212 } 213 } 214 } 215 return Collections.unmodifiableList(endpoints); 216 } 217 218 @Override 219 public Endpoint getLocalEndpoint(QName serviceName) { 220 if (serviceName == null) { 221 throw new IllegalArgumentException("serviceName cannot be null"); 222 } 223 synchronized (serviceLock) { 224 LocalService localService = localServices.get(serviceName); 225 if (localService != null) { 226 return localService.getEndpoint(); 227 } 228 return null; 229 } 230 } 231 232 @Override 233 public Map<QName, Endpoint> getLocalEndpoints() { 234 Map<QName, Endpoint> localEndpoints = new HashMap<QName, Endpoint>(); 235 synchronized (serviceLock) { 236 for (QName localServiceName : localServices.keySet()) { 237 LocalService localService = localServices.get(localServiceName); 238 localEndpoints.put(localServiceName, localService.getEndpoint()); 239 } 240 } 241 return Collections.unmodifiableMap(localEndpoints); 242 } 243 244 @Override 245 public List<Endpoint> getAllEndpoints() { 246 List<Endpoint> allEndpoints = new ArrayList<Endpoint>(); 247 synchronized (serviceLock) { 248 for (QName serviceName : this.localServices.keySet()) { 249 allEndpoints.add(this.localServices.get(serviceName).getEndpoint()); 250 } 251 for (QName serviceName : this.clientRegistryCache.keySet()) { 252 Set<RemoteService> remoteServices = clientRegistryCache.get(serviceName); 253 for (RemoteService remoteService : remoteServices) { 254 allEndpoints.add(remoteService.getEndpoint()); 255 } 256 } 257 } 258 return Collections.unmodifiableList(allEndpoints); 259 } 260 261 @Override 262 public Endpoint getEndpoint(QName serviceName) { 263 return getEndpoint(serviceName, null); 264 } 265 266 @Override 267 public Endpoint getEndpoint(QName serviceName, String applicationId) { 268 if (serviceName == null) { 269 throw new IllegalArgumentException("serviceName cannot be null"); 270 } 271 Endpoint availableEndpoint = null; 272 synchronized (serviceLock) { 273 // look at local services first 274 availableEndpoint = getLocalEndpoint(serviceName); 275 if (availableEndpoint == null || (!StringUtils.isBlank(applicationId) && !availableEndpoint.getServiceConfiguration().getApplicationId().equals(applicationId))) { 276 // TODO - would be better to return an Endpoint that contained an internal proxy to all the services so fail-over would be easier to implement! 277 Set<RemoteService> remoteServices = clientRegistryCache.get(serviceName); 278 remoteServices = filterByApplicationId(applicationId, remoteServices); 279 if (remoteServices != null && !remoteServices.isEmpty()) { 280 // TODO - this should also probably check the current status of the service? 281 RemoteService[] remoteServiceArray = remoteServices.toArray(new RemoteService[0]); 282 RemoteService availableRemoteService = remoteServiceArray[this.randomNumber.nextInt(remoteServiceArray.length)]; 283 availableEndpoint = availableRemoteService.getEndpoint(); 284 } 285 } 286 } 287 return availableEndpoint; 288 } 289 290 protected Set<RemoteService> filterByApplicationId(String applicationId, Set<RemoteService> remoteServices) { 291 if (StringUtils.isBlank(applicationId) || remoteServices == null || remoteServices.isEmpty()) { 292 return remoteServices; 293 } 294 Set<RemoteService> filtered = new HashSet<RemoteService>(); 295 for (RemoteService remoteService : remoteServices) { 296 if (remoteService.getServiceInfo().getApplicationId().equals(applicationId)) { 297 filtered.add(remoteService); 298 } 299 } 300 return filtered; 301 } 302 303 @Override 304 public Endpoint getConfiguredEndpoint(ServiceConfiguration serviceConfiguration) { 305 if (serviceConfiguration == null) { 306 throw new IllegalArgumentException("serviceConfiguration cannot be null"); 307 } 308 synchronized (serviceLock) { 309 Endpoint localEndpoint = getLocalEndpoint(serviceConfiguration.getServiceName()); 310 if (localEndpoint != null && localEndpoint.getServiceConfiguration().equals(serviceConfiguration)) { 311 return localEndpoint; 312 } 313 List<Endpoint> remoteEndpoints = getRemoteEndpoints(serviceConfiguration.getServiceName()); 314 for (Endpoint remoteEndpoint : remoteEndpoints) { 315 if (remoteEndpoint.getServiceConfiguration().equals(serviceConfiguration)) { 316 return remoteEndpoint; 317 } 318 } 319 } 320 return null; 321 } 322 323 @Override 324 public Object getService(QName serviceName) { 325 return getService(serviceName, null); 326 } 327 328 @Override 329 public Object getService(QName serviceName, String applicationId) { 330 Endpoint availableEndpoint = getEndpoint(serviceName, applicationId); 331 if (availableEndpoint == null) { 332 return null; 333 } 334 return availableEndpoint.getService(); 335 } 336 337 @Override 338 public ServiceConfiguration publishService(ServiceDefinition serviceDefinition, boolean synchronize) { 339 if (serviceDefinition == null) { 340 throw new IllegalArgumentException("serviceDefinition cannot be null"); 341 } 342 LocalService localService = new LocalService(getInstanceId(), serviceDefinition); 343 synchronized (serviceLock) { 344 serviceExportManager.exportService(serviceDefinition); 345 localServices.put(serviceDefinition.getServiceName(), localService); 346 } 347 if (synchronize) { 348 synchronize(); 349 } 350 return localService.getEndpoint().getServiceConfiguration(); 351 } 352 353 @Override 354 public List<ServiceConfiguration> publishServices(List<ServiceDefinition> serviceDefinitions, boolean synchronize) { 355 if (serviceDefinitions == null) { 356 throw new IllegalArgumentException("serviceDefinitions list cannot be null"); 357 } 358 List<ServiceConfiguration> serviceConfigurations = new ArrayList<ServiceConfiguration>(); 359 synchronized (serviceLock) { 360 for (ServiceDefinition serviceDefinition : serviceDefinitions) { 361 ServiceConfiguration serviceConfiguration = publishService(serviceDefinition, false); 362 serviceConfigurations.add(serviceConfiguration); 363 } 364 } 365 if (synchronize) { 366 synchronize(); 367 } 368 return Collections.unmodifiableList(serviceConfigurations); 369 } 370 371 @Override 372 public boolean removeService(QName serviceName, boolean synchronize) { 373 if (serviceName == null) { 374 throw new IllegalArgumentException("serviceName cannot be null"); 375 } 376 boolean serviceRemoved = false; 377 synchronized (serviceLock) { 378 LocalService localService = localServices.remove(serviceName); 379 serviceRemoved = localService != null; 380 serviceExportManager.removeService(serviceName); 381 } 382 if (serviceRemoved && synchronize) { 383 synchronize(); 384 } 385 return serviceRemoved; 386 } 387 388 @Override 389 public List<Boolean> removeServices(List<QName> serviceNames, boolean synchronize) { 390 if (serviceNames == null) { 391 throw new IllegalArgumentException("serviceNames cannot be null"); 392 } 393 boolean serviceRemoved = false; 394 List<Boolean> servicesRemoved = new ArrayList<Boolean>(); 395 synchronized (serviceLock) { 396 for (QName serviceName : serviceNames) { 397 serviceExportManager.removeService(serviceName); 398 LocalService localService = localServices.remove(serviceName); 399 if (localService != null) { 400 servicesRemoved.add(Boolean.TRUE); 401 serviceRemoved = true; 402 } else { 403 servicesRemoved.add(Boolean.FALSE); 404 } 405 } 406 } 407 if (serviceRemoved && synchronize) { 408 synchronize(); 409 } 410 return servicesRemoved; 411 } 412 413 protected void synchronizeAndProcess(SyncProcessor processor) { 414 if (!isDevMode()) { 415 synchronized (synchronizeLock) { 416 List<LocalService> localServicesList; 417 List<RemoteService> clientRegistryCacheList; 418 synchronized (serviceLock) { 419 // first, flatten the lists 420 localServicesList = new ArrayList<LocalService>(this.localServices.values()); 421 clientRegistryCacheList = new ArrayList<RemoteService>(); 422 for (Set<RemoteService> remoteServices : this.clientRegistryCache.values()) { 423 clientRegistryCacheList.addAll(remoteServices); 424 } 425 } 426 CompleteServiceDiff serviceDiff = diffCalculator.diffServices(getInstanceId(), localServicesList, clientRegistryCacheList); 427 logCompleteServiceDiff(serviceDiff); 428 processor.sync(serviceDiff); 429 } 430 } 431 } 432 433 @Override 434 public void synchronize() { 435 synchronizeAndProcess(new SyncProcessor() { 436 @Override 437 public void sync(CompleteServiceDiff diff) { 438 RemoteServicesDiff remoteServicesDiff = diff.getRemoteServicesDiff(); 439 processRemoteServiceDiff(remoteServicesDiff); 440 LocalServicesDiff localServicesDiff = diff.getLocalServicesDiff(); 441 processLocalServiceDiff(localServicesDiff); 442 } 443 }); 444 } 445 446 @Override 447 public void synchronizeRemoteServices() { 448 synchronizeAndProcess(new SyncProcessor() { 449 @Override 450 public void sync(CompleteServiceDiff diff) { 451 RemoteServicesDiff remoteServicesDiff = diff.getRemoteServicesDiff(); 452 processRemoteServiceDiff(remoteServicesDiff); 453 } 454 }); 455 } 456 457 @Override 458 public void synchronizeLocalServices() { 459 synchronizeAndProcess(new SyncProcessor() { 460 @Override 461 public void sync(CompleteServiceDiff diff) { 462 LocalServicesDiff localServicesDiff = diff.getLocalServicesDiff(); 463 processLocalServiceDiff(localServicesDiff); 464 } 465 }); 466 } 467 468 protected void logCompleteServiceDiff(CompleteServiceDiff serviceDiff) { 469 RemoteServicesDiff remoteServicesDiff = serviceDiff.getRemoteServicesDiff(); 470 int newServices = remoteServicesDiff.getNewServices().size(); 471 int removedServices = remoteServicesDiff.getRemovedServices().size(); 472 473 LocalServicesDiff localServicesDiff = serviceDiff.getLocalServicesDiff(); 474 int servicesToPublish = localServicesDiff.getLocalServicesToPublish().size(); 475 int servicesToUpdate = localServicesDiff.getLocalServicesToUpdate().size(); 476 int servicesToRemove = localServicesDiff.getServicesToRemoveFromRegistry().size(); 477 478 if (newServices + removedServices + servicesToPublish + servicesToUpdate + servicesToRemove > 0) { 479 LOG.info("Found service changes during synchronization: remoteNewServices=" + newServices + 480 ", remoteRemovedServices=" + removedServices + 481 ", localServicesToPublish=" + servicesToPublish + 482 ", localServicesToUpdate=" + servicesToUpdate + 483 ", localServicesToRemove=" + servicesToRemove); 484 } 485 } 486 487 protected void processRemoteServiceDiff(RemoteServicesDiff remoteServicesDiff) { 488 // note that since there is a gap between when the original services are acquired, the diff, and this subsequent critical section 489 // the list of local and client registry services could have changed, so that needs to be considered in the remaining code 490 synchronized (serviceLock) { 491 // first, let's update what we know about the remote services 492 List<RemoteService> removedServices = remoteServicesDiff.getRemovedServices(); 493 for (RemoteService removedRemoteService : removedServices) { 494 Set<RemoteService> remoteServiceSet = this.clientRegistryCache.get(removedRemoteService.getServiceName()); 495 if (remoteServiceSet != null) { 496 boolean wasRemoved = remoteServiceSet.remove(removedRemoteService); 497 if (!wasRemoved) { 498 LOG.warn("Failed to remove remoteService during synchronization: " + removedRemoteService); 499 } 500 } 501 } 502 List<ServiceInfo> newServices = remoteServicesDiff.getNewServices(); 503 for (ServiceInfo newService : newServices) { 504 Set<RemoteService> remoteServiceSet = clientRegistryCache.get(newService.getServiceName()); 505 if (remoteServiceSet == null) { 506 remoteServiceSet = new HashSet<RemoteService>(); 507 clientRegistryCache.put(newService.getServiceName(), remoteServiceSet); 508 } 509 remoteServiceSet.add(new RemoteService(newService, this.serviceRegistry)); 510 } 511 } 512 } 513 514 protected void processLocalServiceDiff(LocalServicesDiff localServicesDiff) { 515 List<String> removeServiceEndpointIds = new ArrayList<String>(); 516 List<ServiceEndpoint> publishServiceEndpoints = new ArrayList<ServiceEndpoint>(); 517 for (ServiceInfo serviceToRemove : localServicesDiff.getServicesToRemoveFromRegistry()) { 518 removeServiceEndpointIds.add(serviceToRemove.getServiceId()); 519 } 520 for (LocalService localService : localServicesDiff.getLocalServicesToPublish()) { 521 publishServiceEndpoints.add(localService.getServiceEndpoint()); 522 } 523 for (LocalService localService : localServicesDiff.getLocalServicesToUpdate().keySet()) { 524 ServiceInfo registryServiceInfo = localServicesDiff.getLocalServicesToUpdate().get(localService); 525 publishServiceEndpoints.add(rebuildServiceEndpointForUpdate(localService.getServiceEndpoint(), registryServiceInfo)); 526 } 527 boolean batchMode = ConfigContext.getCurrentContextConfig().getBooleanProperty(Config.BATCH_MODE, false); 528 if (!batchMode && (!removeServiceEndpointIds.isEmpty() || !publishServiceEndpoints.isEmpty())) { 529 RemoveAndPublishResult result = this.serviceRegistry.removeAndPublish(removeServiceEndpointIds, publishServiceEndpoints); 530 // now update the ServiceEndpoints for our local services so we can get the proper id for them 531 if (!result.getServicesPublished().isEmpty()) { 532 synchronized (serviceLock) { 533 for (ServiceEndpoint publishedService : result.getServicesPublished()) { 534 rebuildLocalServiceEndpointAfterPublishing(publishedService); 535 } 536 } 537 } 538 } 539 } 540 541 protected ServiceEndpoint rebuildServiceEndpointForUpdate(ServiceEndpoint originalEndpoint, ServiceInfo registryServiceInfo) { 542 ServiceEndpoint.Builder builder = ServiceEndpoint.Builder.create(originalEndpoint); 543 builder.getInfo().setServiceId(registryServiceInfo.getServiceId()); 544 builder.getInfo().setServiceDescriptorId(registryServiceInfo.getServiceDescriptorId()); 545 builder.getDescriptor().setId(registryServiceInfo.getServiceDescriptorId()); 546 return builder.build(); 547 } 548 549 protected void rebuildLocalServiceEndpointAfterPublishing(ServiceEndpoint publishedService) { 550 // verify the service is still published 551 QName serviceName = publishedService.getInfo().getServiceName(); 552 if (localServices.containsKey(serviceName)) { 553 LocalService newLocalService = new LocalService(localServices.get(serviceName), publishedService); 554 localServices.put(serviceName, newLocalService); 555 } 556 } 557 558 public void setServiceRegistry(ServiceRegistry serviceRegistry) { 559 this.serviceRegistry = serviceRegistry; 560 } 561 562 public void setDiffCalculator(ServiceRegistryDiffCalculator diffCalculator) { 563 this.diffCalculator = diffCalculator; 564 } 565 566 public void setServiceExportManager(ServiceExportManager serviceExportManager) { 567 this.serviceExportManager = serviceExportManager; 568 } 569 570 public void setScheduledPool(KSBScheduledPool scheduledPool) { 571 this.scheduledPool = scheduledPool; 572 } 573 574 private static interface SyncProcessor { 575 void sync(CompleteServiceDiff diff); 576 } 577 578}