Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.
/ PesapalSolution Public archive

This is a solution for problem 3 defined in the Pesapal Careers portal for the role for Junior Developer.

Notifications You must be signed in to change notification settings

puumCore/PesapalSolution

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pesapal Solution

This is a Spring boot v3.0 repository which attempts to solve Problem 3 as defined in the Pesapal Careers portal.

How to Install and Run the Project

  1. Clone this repository
  2. Ensure you have Java 17 installed on your machine.
  3. Start your preferred Java supporting IDE e.g. Intellij.
  4. Open this project with the IDE and allow it to load and prepare the project to build with Gradle.
  5. Finally run the Gradle bootRun command.
  6. You can use Postman or cURL to test the end points.

Problem 3: A distributed system.

Build a TCP server that can accept and hold a maximum of N clients (where N is configurable). These clients are assigned ranks based on first-come-first-serve, i.e whoever connects first receives the next available high rank. Ranks are from 0–N, 0 being the highest rank.

Clients can send to the server commands that the server distributes among the clients. Only a client with a lower rank can execute a command of a higher rank client. Higher rank clients cannot execute commands by lower rank clients, so these commands are rejected. The command execution can be as simple as the client printing to console that command has been executed.

If a client disconnects the server should re-adjust the ranks and promote any client that needs to be promoted not to leave any gaps in the ranks.

Solution

The following describes how i tackled the above problem. Let's start with the Endpoints then code, where i will show you how each of the endpoint functions was built.

Endpoints

Base url: http://localhost:8080/pesa-pal/problem3/api/v1.

The following are the resources available from the base url:

  1. Providing a client to the server

    For the server to accept the client, provide the client's name with the request /solution/register as a POST request and add json body similar to the one shown below.

    curl --location --request POST 'http://localhost:8080/pesa-pal/problem3/api/v1/solution/register' \ --header 'Content-Type: application/json' \ --data-raw '{ "client": "mandela" }'

    On success you will get a response with CREATED http status to mean that it was a success otherwise you will get an exception with reason.

    However when you try to add an already existing client you will receive a Bad Request error as shown below.

  2. List clients on the server

    To fetch list of active clients use /solution/clients as a GET request and with a json body as shown below.

    curl --location --request GET 'http://localhost:8080/pesa-pal/problem3/api/v1/solution/clients'

    On success you will get a response with OK http status and json array body with the to mean that it was a success otherwise you will get an exception with reason.

  3. Ping Server

    The client is required to ping the server after every 60 seconds with /solution/online as a POST request and with a json body as shown below.

    curl --location --request POST 'http://localhost:8080/pesa-pal/problem3/api/v1/solution/online' \ --header 'Content-Type: application/json' \ --data-raw '{"client": "mandela"}'

    On success you will get a response with HTTP Status 200 to mean that it was a success otherwise you will get an exception with reason.

    However when the target client doesn't exist you will receive a Not Found error as shown below.

  4. Execute commands

    The client is required to provide a command using /solution/cmd as a POST request and with a json body as shown below.

    curl --location --request POST 'http://localhost:8080/pesa-pal/problem3/api/v1/solution/cmd' \ --header 'Content-Type: application/json' \ --data-raw '{ "client": "perez", "cmd": "Hello world" }'

    On success you will get a response with HTTP Status 200 to mean that it was a success otherwise you will get an exception with reason.

    The following are some of the exceptions that can be thrown:




Code

Project directory structure:

To effectively solve the larger part of the problem, I choose to use the Queue<?> datatype becuse of its elastic ability on the items it holds. Therefore items in the Queue automatically adjusts to the FIFO order.

The following are code snipnnets for important functions in the application:

  1. Service Function Class
    public interface Functions {
    /**
    * Queues are a linear data structure whose operations are performed in FIFO(First In First Out) order.<br>
    * The queue is meant to be universal across its dependants.
    */
    Queue<String> CLIENT_QUEUE = new ConcurrentLinkedQueue<>();
    /**
    * This defines the maximum number of clients that can be accepted.
    */
    int maxClients = 10;
    HashMap<String, LocalDateTime> ONLINE_CLIENTS = new HashMap<>();
    /**
    * This function adds a client name into the queue while checking if thr maximum number of clients has been reached and<br>
    * ensuring that there are no duplicates of the same client name in the queue.
    *
    * @param name The client to add
    * @return True if the client was successfully added.
    */
    boolean add_client(String name);
    /**
    * This function deletes a client from the queue
    *
    * @param name The client to delete
    * @return True if the client was successfully removed
    */
    boolean remove_client(String name);
    /**
    * This function distributes & executes the command provided by the client among the subordinate clients
    *
    * @param client The client who has provided the command
    * @param command What the lower ranked clients should execute
    * @return True if conditions to execute command have been met
    */
    boolean execute(String client, String command);
    /**
    * This function updates the list of online clients who have been inactive for over 60 seconds
    *
    * @return The runnable thread with instructions to remove clients.
    */
    Runnable update_online_clients();
    /**
    * When the client periodically pings the system, their last seen time is updated
    *
    * @param client The client whose last seen time is to be updated
    * @return True if the client's last seen time was successfully updated
    */
    boolean update_client_online_status(String client);
    }
  2. Service Class
    @Service
    @Slf4j
    public class Assistant implements Functions {
    protected final Clock clock = Clock.system(ZoneId.of("Africa/Nairobi"));
    /**
    * This function adds a client name into the queue while ensuring that there are no duplicates of the same client name in the queue.
    *
    * @param name The client to add
    * @return True if the client was successfully added.
    */
    @Override
    public boolean add_client(String name) {
    if (CLIENT_QUEUE.stream().noneMatch(s -> s.equals(name))) {
    if (CLIENT_QUEUE.offer(name)) {
    ONLINE_CLIENTS.put(name, LocalDateTime.now(clock));
    return true;
    } else {
    return false;
    }
    } else {
    return true;
    }
    }
    /**
    * This function deletes a client from the queue
    *
    * @param name The client to delete
    * @return True if the client was successfully removed
    */
    @Override
    public boolean remove_client(String name) {
    if (CLIENT_QUEUE.remove(name)) {
    ONLINE_CLIENTS.remove(name);
    return true;
    }
    return false;
    }
    /**
    * This function distributes & executes the command provided by the client among the subordinate clients
    *
    * @param client The client who has provided the command
    * @param command What the lower ranked clients should execute
    * @return True if conditions to execute command have been met
    */
    @Override
    public boolean execute(String client, String command) {
    var clientRank = CLIENT_QUEUE.stream().toList().indexOf(client);
    if ((clientRank - (CLIENT_QUEUE.size() - 1)) < 0) {
    IntStream.range(0, CLIENT_QUEUE.size())
    .filter(rank -> rank > clientRank)
    .forEach(rank -> log.info("Client '{}'\t|\tExecuting the command '{}' on behalf of the superior client '{}'.", CLIENT_QUEUE.stream().toList().get(rank), command, client));
    return true;
    } else {
    return false;
    }
    }
    /**
    * This function updates the list of online clients who have been inactive for over 60 seconds
    *
    * @return The runnable thread with instructions to remove clients.
    */
    @Override
    public Runnable update_online_clients() {
    return () -> {
    CLIENT_QUEUE.forEach(client -> {
    var localDateTime = ONLINE_CLIENTS.get(client);
    if (localDateTime != null) {
    var duration = Duration.between(localDateTime, LocalDateTime.now(clock));
    log.info("Client '{}' was connected {} seconds ago", client, duration.getSeconds());
    if (duration.getSeconds() >= 60) {
    if (remove_client(client)) {
    log.info("Client '{}' is disconnected", client);
    }
    }
    }
    });
    log.info("'{}' online clients found", (long) ONLINE_CLIENTS.size());
    };
    }
    /**
    * When the client periodically pings the system, their last seen time is updated
    *
    * @param client The client whose last seen time is to be updated
    * @return True if the client's last seen time was successfully updated
    */
    @Override
    public boolean update_client_online_status(String client) {
    return ONLINE_CLIENTS.put(client, LocalDateTime.now(clock)) != null;
    }
    }
  3. Scheduler Class
    @Configuration
    @EnableScheduling
    @Slf4j
    public class Daemon {
    @Autowired
    private Functions functions;
    @Scheduled(initialDelay = 30, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
    void update_active_clients() {
    log.info("Updating connected clients...");
    new Thread(functions.update_online_clients()).start();
    }
    }
  4. Controller Class
    @RestController
    @RequestMapping(path = "solution", produces = MediaType.APPLICATION_JSON_VALUE)
    @Slf4j
    public class Controller {
    @Autowired
    private Functions functions;
    @PostMapping(path = "/register", consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(HttpStatus.CREATED)
    void accept_user(@RequestBody Form.User user) {
    log.info("body = {}", user);
    if (Functions.CLIENT_QUEUE.size() > Functions.maxClients) {
    throw new BadRequestException("Sorry but the maximum number of clients has been reached");
    }
    if (Functions.CLIENT_QUEUE.stream().anyMatch(s -> s.equals(user.client()))) {
    throw new BadRequestException("The desired client is already logged in the system");
    }
    if (!functions.add_client(user.client())) {
    throw new FailureException("Client name could not be added to queue");
    }
    }
    @GetMapping("/clients")
    Queue<String> get_clients() {
    return Functions.CLIENT_QUEUE;
    }
    @PostMapping(path = "/online", consumes = MediaType.APPLICATION_JSON_VALUE)
    void update_client_last_seen(@RequestBody Form.User user) {
    log.info("body = {}", user);
    if (Functions.CLIENT_QUEUE.stream().noneMatch(s -> s.equals(user.client()))) {
    throw new NotFoundException("The bidding client doesn't exist");
    }
    if (!functions.update_client_online_status(user.client())) {
    throw new FailureException("Could not update the client's last seen time");
    }
    }
    @PostMapping(path = "/cmd", consumes = MediaType.APPLICATION_JSON_VALUE)
    void carry_out_client_bidding(@RequestBody Form.Bidding bidding) {
    log.info("body = {}", bidding.toString());
    if (Functions.CLIENT_QUEUE.size() == 1) {
    throw new BadRequestException("Please add one more client to continue");
    }
    if (Functions.CLIENT_QUEUE.stream().noneMatch(s -> s.equals(bidding.client()))) {
    throw new BadRequestException("The commanding client doesn't exist");
    }
    if (!functions.execute(bidding.client(), bidding.cmd())) {
    throw new FailureException("The command has been rejected because there are no subordinate clients to execute your command");
    }
    }
    }

Output

  1. Start up output
  2. On Adding new client
  3. On Disconnecting client after 60 seconds
  4. On Executing Client Command after a client of a higher rank has provided a "bid" for the command

About

This is a solution for problem 3 defined in the Pesapal Careers portal for the role for Junior Developer.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages