All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentcs.strategies.basic.scheduling.modules.SameDirectionBlockModule Maven / Gradle / Ivy

There is a newer version: 6.2.0
Show newest version
/**
 * Copyright (c) The openTCS Authors.
 *
 * This program is free software and subject to the MIT license. (For details,
 * see the licensing information (LICENSE.txt) you should have received with
 * this copy of the software.)
 */
package org.opentcs.strategies.basic.scheduling.modules;

import static java.util.Objects.requireNonNull;
import static org.opentcs.components.kernel.Scheduler.PROPKEY_BLOCK_ENTRY_DIRECTION;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import org.opentcs.components.kernel.Scheduler;
import org.opentcs.components.kernel.services.InternalPlantModelService;
import org.opentcs.customizations.kernel.GlobalSyncObject;
import org.opentcs.data.model.Block;
import org.opentcs.data.model.Path;
import org.opentcs.data.model.TCSResource;
import org.opentcs.strategies.basic.scheduling.ReservationPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Checks if the resources a client may allocate are part of a
 * {@link Block.Type#SAME_DIRECTION_ONLY} block and whether the client is allowed to drive along
 * the block in the requested direction.
 */
public class SameDirectionBlockModule
    implements
      Scheduler.Module {

  /**
   * This class's logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(SameDirectionBlockModule.class);
  /**
   * The reservation pool.
   */
  private final ReservationPool reservationPool;
  /**
   * The plant model service.
   */
  private final InternalPlantModelService plantModelService;
  /**
   * The permissions for all {@link Block.Type#SAME_DIRECTION_ONLY} blocks in a plant model.
   */
  private final Map permissions = new HashMap<>();
  /**
   * A global object to be used for synchronization within the kernel.
   */
  private final Object globalSyncObject;
  /**
   * Whether this module is initialized.
   */
  private boolean initialized;

  @Inject
  public SameDirectionBlockModule(
      @Nonnull
      ReservationPool reservationPool,
      @Nonnull
      InternalPlantModelService plantModelService,
      @GlobalSyncObject
      Object globalSyncObject
  ) {
    this.reservationPool = requireNonNull(reservationPool, "reservationPool");
    this.plantModelService = requireNonNull(plantModelService, "plantModelService");
    this.globalSyncObject = requireNonNull(globalSyncObject, "globalSyncObject");
  }

  @Override
  public void initialize() {
    if (isInitialized()) {
      return;
    }

    Set blocks = plantModelService.fetchObjects(Block.class);
    for (Block block : blocks) {
      if (block.getType() == Block.Type.SAME_DIRECTION_ONLY) {
        permissions.put(block, new BlockPermission(block));
      }
    }

    initialized = true;
  }

  @Override
  public boolean isInitialized() {
    return initialized;
  }

  @Override
  public void terminate() {
    if (!isInitialized()) {
      return;
    }

    permissions.clear();

    initialized = false;
  }

  @Override
  public void setAllocationState(
      Scheduler.Client client,
      Set> alloc,
      List>> remainingClaim
  ) {
  }

  @Override
  public boolean mayAllocate(Scheduler.Client client, Set> resources) {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    synchronized (globalSyncObject) {
      // Other modules may prevented the last allocation, discard any previous requests.
      discardPreviousRequests();

      Set blocks = filterBlocksContainingResources(
          resources,
          Block.Type.SAME_DIRECTION_ONLY
      );
      if (blocks.isEmpty()) {
        LOG.debug("{}: No blocks to be checked, allocation allowed.", client.getId());
        return true;
      }

      Path path = selectPath(resources);
      if (path == null) {
        // If there's no path in the requested resources the vehicle won't move and already has
        // permission to be in the block(s).
        LOG.debug("{}: No path in resources, allocation allowed.", client.getId());
        return true;
      }

      LOG.debug("{}: Checking resource availability: {}", client.getId(), resources);
      if (!checkBlockEntryPermissions(
          client,
          blocks,
          path.getProperties().getOrDefault(PROPKEY_BLOCK_ENTRY_DIRECTION, path.getName())
      )) {
        LOG.debug("{}: Resources unavailable.", client.getId());
        return false;
      }

      LOG.debug("{}: Resources available, allocation allowed.", client.getId());
      return true;
    }
  }

  @Override
  public void prepareAllocation(Scheduler.Client client, Set> resources) {
    permissions.values().forEach(permission -> permission.permitPendingRequests());
  }

  @Override
  public boolean hasPreparedAllocation(Scheduler.Client client, Set> resources) {
    return permissions.values().stream().noneMatch(BlockPermission::hasPendingRequests);
  }

  @Override
  public void allocationReleased(Scheduler.Client client, Set> resources) {
    requireNonNull(client, "client");
    requireNonNull(resources, "resources");

    synchronized (globalSyncObject) {
      for (Map.Entry entry : permissions.entrySet()) {
        Block block = entry.getKey();
        BlockPermission permission = entry.getValue();

        if (!permission.isPermissionGranted(client)) {
          continue;
        }

        if (blockResourcesAllocatedByClient(block, client)) {
          continue;
        }

        // The client released resources and does no longer hold any resources of this block.
        // We don't need permissions any more.
        permission.removePermissionFor(client);
      }
    }
  }

  private void discardPreviousRequests() {
    LOG.debug("Discarding all pending requests...");
    permissions.values().forEach(permission -> permission.clearPendingRequests());
  }

  private Set filterBlocksContainingResources(
      Set> resources,
      Block.Type type
  ) {
    Set result = new HashSet<>();
    Set blocks = plantModelService.fetchObjects(
        Block.class,
        block -> block.getType() == type
    );
    for (TCSResource resource : resources) {
      for (Block block : blocks) {
        if (block.getMembers().contains(resource.getReference())) {
          result.add(block);
        }
      }
    }
    return result;
  }

  @Nullable
  private Path selectPath(Set> resources) {
    for (TCSResource resource : resources) {
      if (resource instanceof Path) {
        return ((Path) resource);
      }
    }

    return null;
  }

  private boolean checkBlockEntryPermissions(
      Scheduler.Client client,
      Set blocks,
      String entryDirection
  ) {
    LOG.debug(
        "{}: Checking entry permissions for blocks '{}' with entry direction '{}'.",
        client.getId(),
        entryDirection
    );
    boolean entryPermissible = true;
    for (Block block : blocks) {
      entryPermissible &= permissions.get(block).enqueueRequest(client, entryDirection);
    }

    return entryPermissible;
  }

  private boolean blockResourcesAllocatedByClient(Block block, Scheduler.Client client) {
    Set clientBlocks
        = filterBlocksContainingResources(
            reservationPool.allocatedResources(client),
            Block.Type.SAME_DIRECTION_ONLY
        );
    return clientBlocks.contains(block);
  }

  /**
   * Manages the clients that are permitted to drive along a block by considering the direction
   * clients request to enter the block.
   */
  private class BlockPermission {

    /**
     * The block to manage permissions for.
     */
    private final Block block;
    /**
     * The clients permitted to drive along the block.
     */
    private final Set clients = new HashSet<>();
    /**
     * The direction vehicles are allowed to enter the block.
     */
    @Nullable
    private String entryDirection;
    /**
     * The queue of pending permission requests.
     */
    private final Queue pendingRequests = new ArrayDeque<>();

    BlockPermission(Block block) {
      this.block = requireNonNull(block, "block");
    }

    public void permitPendingRequests() {
      while (hasPendingRequests()) {
        PermissionRequest request = pendingRequests.poll();

        if (clientAlreadyInBlock(request.getClient())) {
          LOG.debug(
              "Permission for block {} already granted to {}.",
              block.getName(),
              request.getClient().getId()
          );
        }
        else if (entryPermissible(request.getEntryDirection())) {
          clients.add(request.getClient());
          this.entryDirection = request.getEntryDirection();
          LOG.debug(
              "Permission for block {} granted to {} (entryDirection={}).",
              block.getName(),
              request.getClient().getId(),
              request.getEntryDirection()
          );
        }
      }
    }

    public boolean enqueueRequest(Scheduler.Client client, String entryDirection) {
      if (clientAlreadyInBlock(client)
          || entryPermissible(entryDirection)) {
        LOG.debug(
            "Enqueuing permission request for block {} to {} with entry direction '{}'.",
            block.getName(),
            client.getId(),
            entryDirection
        );
        pendingRequests.add(new PermissionRequest(client, entryDirection));
        return true;
      }

      LOG.debug(
          "Client {} not permissible to block {} with entry direction '{}' (!= '{}').",
          client.getId(),
          block.getName(),
          entryDirection,
          this.entryDirection
      );
      return false;
    }

    public void clearPendingRequests() {
      pendingRequests.clear();
    }

    public void removePermissionFor(Scheduler.Client client) {
      clients.remove(client);

      if (clients.isEmpty()) {
        entryDirection = null;
      }
    }

    public boolean isPermissionGranted(Scheduler.Client client) {
      return clients.contains(client);
    }

    private boolean hasPendingRequests() {
      return !pendingRequests.isEmpty();
    }

    private boolean clientAlreadyInBlock(Scheduler.Client client) {
      return isPermissionGranted(client);
    }

    private boolean entryPermissible(String entryDirection) {
      return this.entryDirection == null
          || Objects.equals(this.entryDirection, entryDirection);
    }
  }

  private class PermissionRequest {

    /**
     * The requesting client.
     */
    private final Scheduler.Client client;
    /**
     * The entry direction permission is requested for.
     */
    private final String entryDirection;

    /**
     * Creates a new instance.
     *
     * @param client The requesting client.
     * @param entryDirection The entry direction permission is requested for.
     * @param blocks The blocks the client requests permission for.
     */
    PermissionRequest(Scheduler.Client client, String entryDirection) {
      this.client = requireNonNull(client, "client");
      this.entryDirection = requireNonNull(entryDirection, "entryDirection");
    }

    public Scheduler.Client getClient() {
      return client;
    }

    public String getEntryDirection() {
      return entryDirection;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy