diff --git a/CMakeLists.txt b/CMakeLists.txt index 632fe68..d4050bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,13 @@ cmake_minimum_required(VERSION 3.10) -project(ProcessDemo) +project(Exchange LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Find required packages +find_package(tinyxml2 REQUIRED) +find_package(Threads REQUIRED) + # Include common directory include_directories(${CMAKE_SOURCE_DIR}/common) @@ -14,28 +18,47 @@ set(IPC_SOURCES common/ipc/SharedMemory_Consumer.cpp ) +# Common non-header sources (scheduler, workers, etc.) +set(COMMON_SOURCES + common/Scheduler/Scheduler.cpp + common/Scheduler/Worker/Worker.cpp +) + +# Gateway network sources +set(GATEWAY_NETWORK_SOURCES + Gateway/Network/TcpEpollListener.cpp +) + # Process 1 executable (Gateway) -add_executable(Gateway Gateway/main.cpp ${IPC_SOURCES}) +add_executable(Gateway Gateway/main.cpp Gateway/Gateway.cpp ${IPC_SOURCES} ${COMMON_SOURCES} ${GATEWAY_NETWORK_SOURCES}) target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/common) target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/common/ipc) target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/common/Logger) -target_link_libraries(Gateway pthread) +target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/common/xml) +target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/common/Scheduler) +target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/Gateway) +target_include_directories(Gateway PRIVATE ${CMAKE_SOURCE_DIR}/Gateway/Network) + +target_link_libraries(Gateway PRIVATE tinyxml2 Threads::Threads) # Process 2 executable -add_executable(process2 process2/main.cpp ${IPC_SOURCES}) +add_executable(process2 process2/main.cpp ${IPC_SOURCES} ${COMMON_SOURCES}) target_include_directories(process2 PRIVATE ${CMAKE_SOURCE_DIR}/common) target_include_directories(process2 PRIVATE ${CMAKE_SOURCE_DIR}/common/ipc) target_include_directories(process2 PRIVATE ${CMAKE_SOURCE_DIR}/common/Logger) +target_link_libraries(process2 PRIVATE Threads::Threads) # Test executable - IPC Queue Connection and Crash Recovery Test -add_executable(test_ipc_crash tests/test_ipc_crash.cpp ${IPC_SOURCES}) +add_executable(test_ipc_crash tests/test_ipc_crash.cpp ${IPC_SOURCES} ${COMMON_SOURCES}) target_include_directories(test_ipc_crash PRIVATE ${CMAKE_SOURCE_DIR}/common) target_include_directories(test_ipc_crash PRIVATE ${CMAKE_SOURCE_DIR}/common/ipc) +target_link_libraries(test_ipc_crash PRIVATE Threads::Threads) # Test executable - Gateway Network and FIX Protocol Test add_executable(test_gateway tests/test_gateway.cpp) target_include_directories(test_gateway PRIVATE ${CMAKE_SOURCE_DIR}/common) target_include_directories(test_gateway PRIVATE ${CMAKE_SOURCE_DIR}/common/Logger) +target_link_libraries(test_gateway PRIVATE Threads::Threads) # Enable testing enable_testing() diff --git a/Gateway/Config.h b/Gateway/Config.h new file mode 100644 index 0000000..fa8f1cf --- /dev/null +++ b/Gateway/Config.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include "XMLReader.h" +#include "String.h" + +namespace Exchange::Gateway { + class Config : public Core::XMLNode { + Core::String mPort; + Core::String mBlockingQueueSize; + Core::String mMaxFixEventSize; + Core::String mBacklogSize; + + Config(const tinyxml2::XMLElement* element):XMLNode(element){ + mPort = getChild("Port").get(); + mBlockingQueueSize = getChild("BlockingQueue").getChild("Size").get(); + mMaxFixEventSize = getChild("Fix").getChild("MaxEventSize").get(); + mBacklogSize = getChild("Fix").getChild("BacklogSize").get(); + } + public: + // Delete copy and move constructor to enforce singleton + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + + // Lazy initialization + static void init(const tinyxml2::XMLElement* element) { + static Config instance(element); + getInstance() = &instance; + } + + // Accessor + static Config& instance() { + Config* inst = getInstance(); + if (!inst) { + ENG_THROW("Gateway::Config accessed before init()"); + } + return *inst; + } + + // Getters + size_t port() const {return std::stoul(mPort.toString());} + size_t blockingQueueSize() const {return std::stoul(mBlockingQueueSize.toString());} + size_t maxFixEventSize() const {return std::stoul(mMaxFixEventSize.toString());} + size_t backlogSize() const {return std::stoul(mBacklogSize.toString());} + + private: + static Config*& getInstance() { + static Config* instance = nullptr; + return instance; + } + }; +} // namespace Exchange::Gateway \ No newline at end of file diff --git a/Gateway/Fix.h b/Gateway/Fix.h deleted file mode 100644 index 13e9c27..0000000 --- a/Gateway/Fix.h +++ /dev/null @@ -1,268 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Linux specific headers for networking and threading -#include -#include -#include -#include -#include -#include - -#include "Logger/Logger.h" -#include "BlockingQueue/MutexBlockingQueue.h" -#include "Exception.h" - -namespace Exchange::Gateway { - - constexpr char FIX_DELIMITER = '\x01'; // SOH character standard in FIX - constexpr int MAX_EVENTS = 100; - constexpr int PORT = 9000; - constexpr int BUFFER_SIZE = 4096; - - // void pinThreadToCpuCore(std::thread &t, int coreId) { - // cpu_set_t cpuset; - // CPU_ZERO(&cpuset); - // CPU_SET(coreId, &cpuset); - // pthread_t current_thread = pthread_self(); - // pthread_setaffinity_np(t.native_handle(), sizeof(cpu_set_t), &cpuset); - // LOG_INFO("Thread pinned to Core %d",coreId); - // } - - void pinThreadToCore(int coreId) { - cpu_set_t cpuset; - CPU_ZERO(&cpuset); - CPU_SET(coreId, &cpuset); - pthread_t current_thread = pthread_self(); - pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset); - LOG_INFO("Thread pinned to Core %d",coreId); - } - // Global queue to pass raw data from Network to Logic - struct RawPacket { - int clientSocket; - Core::String data; - }; - - Core::IBlockingQueue* gIngressQueue = new Core::MutexBlockingQueue(4096); - - struct FixMessage { - std::string msgType; // Tag 35 - std::string symbol; // Tag 55 - std::string side; // Tag 54 - double price; // Tag 44 - int quantity; // Tag 38 - bool isValid; - }; - - // A "Zero-Copy" style parser would be faster, but string split is easier to understand - FixMessage parseFix(const std::string& raw) { - - LOG_TRACE(static_cast(raw)); - - FixMessage msg; - msg.isValid = false; - - std::stringstream ss(raw); - std::string segment; - - // Split by SOH delimiter (\x01) - while (std::getline(ss, segment, FIX_DELIMITER)) { - size_t eq_pos = segment.find('='); - if (eq_pos == std::string::npos) continue; - - std::string tag = segment.substr(0, eq_pos); - std::string value = segment.substr(eq_pos + 1); - - if (tag == "35") msg.msgType = value; - else if (tag == "55") msg.symbol = value; - else if (tag == "54") msg.side = value; // 1=Buy, 2=Sell - else if (tag == "44") msg.price = std::stod(value); - else if (tag == "38") msg.quantity = std::stoi(value); - } - - // Basic validation: Must have MsgType - if (!msg.msgType.empty()) { - msg.isValid = true; - } - return msg; - } - - void networkLoop() { - // Force this specific thread to run only on CPU Core #1 - // If the Operating System moves this thread to a different core (context switch), - // the CPU cache has to be reloaded, costing microseconds. In trading, we want - // "Hot Cache" at all times. - // pinThreadToCore(1); - - // Address Family: AF_INET, tells OS we are using IPv4 - // Socket Type: SOCK_STREAM, tells OS we want TCP - // Protocol: 0, lets OS choose the default protocol for given family and type - int serverFd = socket(AF_INET, SOCK_STREAM, 0); - - // Setting the strcuture that holds the configration - // - sockaddr_in: A struct specifically designed for IPv4 addresses. - // - INADDR_ANY: This is a shortcut. It tells the server to listen on all available network interfaces. - // If your server has a Local IP (127.0.0.1) and a Public Wi-Fi IP (192.168.1.5), - // INADDR_ANY will accept connections arriving at either IP. - // - htons(PORT): Host to Network Short - // Computers store numbers in different orders (Endianness). Intel CPUs are "Little Endian" (start with - // the smallest byte). The Internet is "Big Endian" (start with the largest byte). If you don't use htons, - // Port 9000 might be interpreted by the network as Port 36899. This function flips the bytes so the network - // understands the number correctly. - sockaddr_in address{}; - address.sin_family = AF_INET; - address.sin_addr.s_addr = INADDR_ANY; - address.sin_port = htons(PORT); - - // Sets REUSEADDR - // When you stop a server (or it crashes), the OS puts the port (e.g., 9000) into a TIME_WAIT state for - // about 60 seconds to ensure all lingering packets are flushed. During this time, you cannot restart - // your server; you will get an "Address already in use" error. - // SO_REUSEADDR tells the OS: "I know this port is technically busy cleaning up, but let me bind to - // it anyway immediately." - // Without this, every time you re-compile and run your code, you'd have to wait 1 minute for the port - // to free up. - int opt = 1; - setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - // Connects the abstract socket ID (serverFd) to the specific IP and Port configuration (address) - if (bind(serverFd, (struct sockaddr*)&address, sizeof(address)) < 0) { - // Another program is already using that port. (as bind returns < 0) - // Or You are trying to use a restricted port (0–1023) without - // Root/Administrator privileges. - perror("Bind failed"); return; - } - - // When the socket is made the socket could be used to make outgoing calls. - // listen flips the switch telling the OS "This socket is not making calls, but waiting for them." - // Puts the socket in passive mode, ready to accept incoming connection requests. - // Backlog queue size: 10 - // If 15 clients try to connect at the exact same nanosecond, The first 10 go into a holding queue. - // The 11th through 15th receive a "Connection Refused" error immediately. - listen(serverFd, 10); - - /* - This block sets up the "Traffic Control Tower" for your network thread. Instead of checking - every connection one by one to see if they have data (which is slow), you are asking the Linux - Kernel to watch them for you. - You can think of this ID as a handle to a "To-Do List" managed by the Operating System. - */ - // Create Epoll Instance - int epollFd = epoll_create1(0); // ID of that epoll instance - - // This declares two variables: - // - event (Singular): This is a Configuration Form. We use this temporarily to describe what we want to - // watch (e.g., "Watch Socket 5 for incoming data"). - // - events (Array): This is the Inbox. When we later call epoll_wait(), the Kernel will - // fill this array with the list of sockets that actually have activity. If 5 clients - // send orders at once, this array will contain 5 items. - epoll_event event{}, events[MAX_EVENTS]; - - // This stands for "Epoll Input". It tells the Kernel: "Wake me up if there is data to read." - // - For a Client Socket: "Data to read" means they sent a FIX message. - // - For the Server Socket: "Data to read" means a new user is trying to connect (the TCP 3-way handshake is complete). - event.events = EPOLLIN; // Read available - - // We attach the ID of the socket (serverFd) to this event. When the event - // fires later, the Kernel will hand this ID back to us so we know who triggered the event. - event.data.fd = serverFd; - - // Registering with the Kernel - // This is the actual system call that submits the form. - // - EPOLL_CTL_ADD: The Command. We are Adding a new socket to the watch list. - // - epollFd: The Traffic Tower instance we created in step 1. - // - serverFd: The target socket we want to watch. - // - &event: The rules we defined in step 3 (Watch for EPOLLIN). - // "Hey Linux (epollFd), please add (ADD) the Main Server Phone (serverFd) to your - // watch list. Use the rules written on this form (&event), which say you should notify - // me whenever it rings (EPOLLIN)." - epoll_ctl(epollFd, EPOLL_CTL_ADD, serverFd, &event); - - LOG_INFO("Gateway is listening on port, %d", PORT); - - // This creates a raw memory array (usually 4KB or 8KB) on the stack. - // When data eventually arrives, we will use read() to pour the raw bytes from the - // network card into this bucket so our application can process them. - // Without epoll, you would have to loop through every client constantly: - // Are you sending data? No. Are you sending data? No. Are you sending data? No. - // With epoll, your code does nothing (consumes 0 CPU) until the Kernel taps it on the shoulder and says: - // - "Socket 7 and Socket 12 have data. Here is the list." - char buffer[BUFFER_SIZE]; - - // Main event loop (Engine of your network thread) - // This loop runs millions of times a day. It sleeps when the market is quiet and - // wakes up instantly when activity happens. - while (true) { - // Wait for events - // Blocking call, wakes up on network activity - // The pause button - // The Linux Kernel holds the thread here until at least one interesting thing happens (e.g., a client sends an order, or a new user tries to connect). - - int eventcount = epoll_wait(epollFd, events, MAX_EVENTS, -1); - - for (int i = 0; i < eventcount; i++) { - - // New Connection - if (events[i].data.fd == serverFd) { - int newSocket = accept(serverFd, nullptr, nullptr); - - // Set non-blocking for new client - fcntl(newSocket, F_SETFL, O_NONBLOCK); - - event.events = EPOLLIN | EPOLLET; // Edge Triggered - event.data.fd = newSocket; - epoll_ctl(epollFd, EPOLL_CTL_ADD, newSocket, &event); - LOG_INFO("New Client Connected:, %d", newSocket); - } - // Data arrived - else { - int clientFd = events[i].data.fd; - int bytesRead = read(clientFd, buffer, BUFFER_SIZE); - - if (bytesRead <= 0) { - // Client disconnected - close(clientFd); - epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr); - } - else { - std::string rawData(buffer, bytesRead); - gIngressQueue->push({clientFd, rawData}); - } - } - } - } - } - - - void consumerLoop() { - // pinThreadToCore(2); - LOG_INFO("Consumer started.."); - - while(true) { - RawPacket packet; - gIngressQueue->pop(packet); - FixMessage fix = parseFix(packet.data.toString()); - - if (fix.isValid) { - if (fix.msgType == "D") { // New Order Single - LOG_INFO("ORDER RECEIVED Client: %d BUY: %d %s @ %.2f", packet.clientSocket, fix.quantity, fix.symbol.c_str(), fix.price); - // todo: send to scheduler process using IPC - } - else if (fix.msgType == "A") { // Logon - LOG_INFO("LOGON Request from Client %d", packet.clientSocket); - } - } - else { - LOG_WARN("Noise or Partial Packet"); - } - } - } -} diff --git a/Gateway/FixMessageDispatcher.h b/Gateway/FixMessageDispatcher.h new file mode 100644 index 0000000..11b1971 --- /dev/null +++ b/Gateway/FixMessageDispatcher.h @@ -0,0 +1,76 @@ +#pragma once + +#include "FIX.h" + +namespace Exchange::Gateway { + + class FixMessageDispatcher { + public: + + /** @brief Constructor */ + FixMessageDispatcher(auto q):mIngesssQueue(std::move(q)){} + + /** + * @brief Main consumer loop. Continuously consumes raw packets, + * parses FIX messages, and dispatches them based on MsgType. + */ + void run() { + LOG_INFO("Fix message dispatcher started"); + while (true) { + Network::RawPacket packet; + // Blocks until data is available or queue is closed + if (!mIngesssQueue->pop(packet)) { + LOG_INFO("Ingress queue closed and empty, dispatcher exiting"); + break; + } + dispatch(packet); + } + } + + private: + + std::shared_ptr> mIngesssQueue; + + void dispatch(const Network::RawPacket& packet) { + Network::Fix::FixMsg fix = Network::Fix::parseFix(packet.data.toString()); + + if (!fix.isValid) { + LOG_WARN("Invalid or partial FIX message from client %d", packet.clientSocket); + return; + } + + // New Order Single + if (fix.msgType == "D") { + handleNewOrder(packet, fix); + } + // Logon: message that establishes a FIX session between two counterparties. + else if (fix.msgType == "A") { + handleLogon(packet); + } + else { + LOG_WARN("Unhandled FIX MsgType=%ls from client %d", fix.msgType.get(), packet.clientSocket); + } + } + + void handleNewOrder(const Network::RawPacket& packet, const Network::Fix::FixMsg& fix) { + LOG_TRACE( + "ORDER RECEIVED Client=%d Side=%s Qty=%d Symbol=%s Price=%.2f", + packet.clientSocket, + fix.side.get(), + fix.quantity, + fix.symbol.get(), + fix.price + ); + // TODO: + // Send order to scheduler / matching engine via IPC + } + + void handleLogon(const Network::RawPacket& packet) { + LOG_INFO("LOGON request from client %d", packet.clientSocket); + // TODO: + // Initialize FIX session state + // Send Logon response + } + }; + +} // namespace Exchange::Gateway::Fix \ No newline at end of file diff --git a/Gateway/Gateway.cpp b/Gateway/Gateway.cpp new file mode 100644 index 0000000..72a1206 --- /dev/null +++ b/Gateway/Gateway.cpp @@ -0,0 +1,85 @@ +#include "Gateway.h" + +#include +#include +#include + + + +namespace Exchange::Gateway { + + static Gateway* gInstance = nullptr; + + Gateway::Gateway(const Core::String& name) + : mName(name) { + gInstance = this; + } + + void Gateway::setupSignalHandlers() { + struct sigaction sa{}; + sa.sa_handler = Gateway::signalHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + LOG_INFO("Signal handlers registered (Ctrl+C to shutdown)"); + } + + void Gateway::signalHandler(int signum) { + if (!gInstance) return; + + if (signum == SIGINT || signum == SIGTERM) { + LOG_INFO("Shutdown signal received"); + gInstance->stop(); + } + } + + void Gateway::start() { + LOG_INFO("Launching Gateway..."); + setupSignalHandlers(); + + // Load configuration + Core::XMLReader reader("../config.xml"); + Config::init(reader.getNode(mName)); + + // Create components + mScheduler = std::make_unique(mName); + + mIngressQueue = + std::make_shared>( + Config::instance().blockingQueueSize() + ); + + mListener = std::make_unique(mIngressQueue); + mDispatcher = std::make_unique(mIngressQueue); + + LOG_INFO("Starting Gateway Scheduler..."); + mScheduler->start(*mListener, *mDispatcher); + + LOG_INFO("Gateway is running. Press Ctrl+C to shutdown."); + + // Main wait loop + while (!mShutdownRequested.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + LOG_INFO("Shutdown initiated, exiting in 1 second..."); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::exit(0); + } + + void Gateway::stop() { + mShutdownRequested.store(true, std::memory_order_release); + + // Safety net: force exit if shutdown hangs + std::thread([] { + std::this_thread::sleep_for(std::chrono::seconds(3)); + LOG_WARN("Force exit after timeout"); + std::exit(0); + }).detach(); + } + +} // namespace Exchange::Gateway diff --git a/Gateway/Gateway.h b/Gateway/Gateway.h new file mode 100644 index 0000000..38cfeb4 --- /dev/null +++ b/Gateway/Gateway.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include "String.h" +#include "Exception.h" +#include "Config.h" +#include "Scheduler/GatewayScheduler.h" +#include "BlockingQueue/MutexBlockingQueue.h" +#include "Network/TcpEpollListener.h" +#include "FixMessageDispatcher.h" + +namespace Exchange::Gateway { + class Gateway { + public: + explicit Gateway(const Core::String& name); + + void start(); + void stop(); + + private: + void setupSignalHandlers(); + static void signalHandler(int signum); + + private: + Core::String mName; + std::atomic mShutdownRequested{false}; + + std::unique_ptr mScheduler; + std::shared_ptr> mIngressQueue; + std::unique_ptr mListener; + std::unique_ptr mDispatcher; + }; +} \ No newline at end of file diff --git a/Gateway/Network/FIX.h b/Gateway/Network/FIX.h new file mode 100644 index 0000000..a5cfddd --- /dev/null +++ b/Gateway/Network/FIX.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include "String.h" + +namespace Exchange::Gateway::Network { + + // Start of Heading (SOH) character standard in FIX (Financial Information eXchange) protocol + // Example: 8=FIX.4.4\x019=176\x0135=D\x0149=BUYER\x0156=SELLER\x0110=128\x01 + constexpr char gFixDelimiter = '\x01'; + + class Fix { + public: + /** + * @struct FixMsg + * @brief Represents a simplified FIX message. + */ + struct FixMsg { + Core::String msgType; // Tag 35: Message type (e.g., "D" = New Order Single) + Core::String symbol; // Tag 55: Financial instrument symbol + Core::String side; // Tag 54: Order side ("1" = Buy, "2" = Sell) + double price; // Tag 44: Order price + int quantity; // Tag 38: Order quantity + bool isValid; // Indicates whether the FIX message passed validation + }; + + /** + * @brief Parses a raw FIX message string into a FixMessage structure. + * + * @details + * The message is split using the SOH (0x01) field delimiter and each tag=value + * pair is extracted and mapped to known FIX tags. A zero-copy parser would avoid + * string allocations and be faster, but this approach is easier to understand + * and maintain. + * @param raw Raw string received from network + * @return + */ + static FixMsg parseFix(const Core::String& raw) { + LOG_TRACE("Raw Fix: %ls", raw); + + FixMsg msg; + msg.isValid = false; + std::stringstream ss(raw.toString()); + std::string segment; + + // Split by SOH delimiter + while (std::getline(ss, segment, gFixDelimiter)) { + size_t eq_pos = segment.find('='); + if (eq_pos == std::string::npos) continue; + + std::string tag = segment.substr(0, eq_pos); + std::string value = segment.substr(eq_pos + 1); + + if (tag == "35") msg.msgType = value; + else if (tag == "55") msg.symbol = value; + else if (tag == "54") msg.side = value; // 1=Buy, 2=Sell + else if (tag == "44") msg.price = std::stod(value); + else if (tag == "38") msg.quantity = std::stoi(value); + } + + // Basic validation + // todo: Add more validation logic + if (!msg.msgType.empty()) { + msg.isValid = true; + } + + return msg; + } + }; +} \ No newline at end of file diff --git a/Gateway/Network/TcpEpollListener.cpp b/Gateway/Network/TcpEpollListener.cpp new file mode 100644 index 0000000..5472d18 --- /dev/null +++ b/Gateway/Network/TcpEpollListener.cpp @@ -0,0 +1,92 @@ + +#include "TcpEpollListener.h" + +namespace Exchange::Gateway::Network { + + static Config& gCfg() { return Config::instance(); } + + void TcpEpollListener::run(std::atomic* stopFlag) { + setupServer(); + eventLoop(stopFlag); + shutdown(); + } + + void TcpEpollListener::setupServer() { + mServerFd = socket(AF_INET, SOCK_STREAM, 0); + + int opt = 1; + setsockopt(mServerFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(gCfg().port()); + + if (bind(mServerFd, (sockaddr*)&address, sizeof(address)) < 0) { + perror("bind"); + throw std::runtime_error("Server bind failed"); + } + + listen(mServerFd, 10); + + mEpollFd = epoll_create1(0); + + epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = mServerFd; + epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mServerFd, &event); + } + + void TcpEpollListener::eventLoop(std::atomic* stopFlag) { + epoll_event events[gCfg().maxFixEventSize()]; + char buffer[1000]; + + while (!stopFlag->load(std::memory_order_acquire)) { + + int count = epoll_wait(mEpollFd, events, gCfg().maxFixEventSize(), 1000); + if (count <= 0) continue; + + for (int i = 0; i < count; ++i) { + int fd = events[i].data.fd; + + if (fd == mServerFd) { + handleAccept(); + } else { + handleRead(fd); + } + } + } + } + + void TcpEpollListener::handleAccept() { + int clientFd = accept(mServerFd, nullptr, nullptr); + if (clientFd < 0) return; + + fcntl(clientFd, F_SETFL, O_NONBLOCK); + + epoll_event event{}; + event.events = EPOLLIN | EPOLLET; + event.data.fd = clientFd; + + epoll_ctl(mEpollFd, EPOLL_CTL_ADD, clientFd, &event); + } + + void TcpEpollListener::handleRead(int clientFd) { + char buffer[1000]; + int bytesRead = read(clientFd, buffer, sizeof(buffer)); + + if (bytesRead <= 0) { + close(clientFd); + epoll_ctl(mEpollFd, EPOLL_CTL_DEL, clientFd, nullptr); + return; + } + + mIngesssQueue->push({clientFd, std::string(buffer, bytesRead)}); + } + + void TcpEpollListener::shutdown() { + close(mEpollFd); + close(mServerFd); + mIngesssQueue->close(); + } +} \ No newline at end of file diff --git a/Gateway/Network/TcpEpollListener.h b/Gateway/Network/TcpEpollListener.h new file mode 100644 index 0000000..04a37d6 --- /dev/null +++ b/Gateway/Network/TcpEpollListener.h @@ -0,0 +1,134 @@ +#pragma once + +#include + +// Linux specific headers for networking and threading +#include +#include +#include +#include +#include +#include + +#include "Exception.h" +#include "BlockingQueue/IBlockingQueue.h" +#include "../Config.h" + +namespace Exchange::Gateway::Network { + + extern Config& gc; + + struct RawPacket { + int clientSocket; + Core::String data; + }; + + class TcpEpollListener { + public: + using BlockingQueue = std::shared_ptr>; + + /** @brief Constructor */ + TcpEpollListener(BlockingQueue q):mIngesssQueue(std::move(q)){} + + void run(std::atomic* stopFlag); + + private: + BlockingQueue mIngesssQueue; + int mServerFd{-1}; + int mEpollFd{-1}; + + void setupServer(); + void eventLoop(std::atomic* stopFlag); + + void handleAccept(); + void handleRead(int clientFd); + + void shutdown(); + // /** + // * @brief Runs the main TCP networking event loop using epoll. + // * + // * @details + // * This function initializes a TCP server socket, binds it to the configured + // * port, and registers it with the Linux epoll subsystem to efficiently monitor + // * network activity. The server socket accepts incoming client connections, + // * while connected client sockets are monitored for incoming data. + // * + // * The loop blocks inside epoll_wait(), consuming no CPU when idle, and wakes + // * only when meaningful network events occur (new connections or incoming data). + // * Incoming bytes are read in non-blocking mode and forwarded to a shared + // * ingress queue for downstream processing (e.g., FIX parsing and validation). + // * + // * The design prioritizes low latency and scalability by avoiding per-connection + // * polling, minimizing context switches, and delegating protocol handling to + // * downstream components. + // * + // * The loop continues running until the provided stop flag is set, after which + // * all sockets are closed and the listener shuts down cleanly. + // */ + // void networkLoop() { + // // Config& config = Config::instance(); + // // Create a TCP socket using IPv4 + // // AF_INET -> IPv4 + // // SOCK_STREAM -> TCP + // // Protocol 0 -> Let the OS select the default protocol + // int serverFd = socket(AF_INET, SOCK_STREAM, 0); + // if (serverFd < 0) { + // ENG_THROW("Socket creation failed") + // return; + // } + + // // Enable SO_REUSEADDR to allow immediate reuse of the port after shutdown. + // // Without this option, the OS may keep the port in TIME_WAIT state for 60 sec, + // // preventing quick restarts during development or recovery. + // int opt = 1; + // setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // // Configure the server address + // // - sockaddr_in is the IPv4-specific socket structure + // // - INADDR_ANY allows listening on all available network interfaces + // // - htons() converts the port from host byte order to network byte order + // // (network byte order is big-endian) + // sockaddr_in address{}; + // address.sin_family = AF_INET; + // address.sin_addr.s_addr = INADDR_ANY; + // address.sin_port = htons(PORT); + + // // Bind the socket to the configured IP address and port + // if (bind(serverFd, (struct sockaddr*)&address, sizeof(address)) < 0) { + // ENG_THROW("Bind failed"); + // return; + // } + + // // Put the socket into passive mode so it can accept incoming connections. + // // The backlog defines how many pending connections can wait in the queue + // // before new connection attempts are rejected. + // listen(serverFd, 10); + + // /** + // * Create an epoll instance to efficiently monitor multiple sockets. + // * epoll allows the kernel to notify us only when meaningful events occur, + // * avoiding inefficient polling and unnecessary CPU usage. + // */ + // int epollFd = epoll_create1(0); + + // // epoll_event: + // // - event → configuration used when registering a socket + // // - events → array filled by the kernel with sockets that have activity + // epoll_event event{}, events[MAX_EVENTS]; + + // // EPOLLIN means "data available to read" + // // - For the server socket: a new client connection is ready to accept + // // - For client sockets: incoming data is available + // event.events = EPOLLIN; + // event.data.fd = serverFd; + + // // Register the server socket with epoll + // epoll_ctl(epollFd, EPOLL_CTL_ADD, serverFd, &event); + + // LOG_INFO("Gateway is listening on port %d", PORT); + + // // Buffer used to read raw bytes from client sockets + // char buffer[BUFFER_SIZE]; + // } + }; +} \ No newline at end of file diff --git a/Gateway/Scheduler/GatewayScheduler.h b/Gateway/Scheduler/GatewayScheduler.h new file mode 100644 index 0000000..9610e8a --- /dev/null +++ b/Gateway/Scheduler/GatewayScheduler.h @@ -0,0 +1,66 @@ +#pragma once + +#include "Scheduler.h" +#include "String.h" +#include "Network/TcpEpollListener.h" +#include "FixMessageDispatcher.h" + +namespace Exchange::Gateway { + + enum Threads { + Listener, + Dispatcher + }; + + class GatewayScheduler : public Scheduler { + Core::String mWorkerPrefix; + size_t mWorkerCnt; + std::map mThreads; + std::shared_ptr> mStopNetwork; + + public: + GatewayScheduler(const Core::String prefix) : mWorkerPrefix(std::move(prefix)), + mStopNetwork(std::make_shared>(false)) + { + mThreads.insert({Threads::Listener, (mWorkerPrefix + "_listener").toString()}); + mThreads.insert({Threads::Dispatcher, (mWorkerPrefix + "_dispatcher").toString()}); + + createWorker(mThreads[Threads::Listener]); + createWorker(mThreads[Threads::Dispatcher]); + } + + void start(Network::TcpEpollListener& listener, Exchange::Gateway::FixMessageDispatcher& dispatcher) { + LOG_INFO("Starting Gateway Scheduler workers..."); + Scheduler::start(); + + auto stopNet = mStopNetwork; + + submitTo( + mThreads[Threads::Listener], + [stopNet, &listener](const CancelToken& token) { + listener.run(stopNet.get()); + }, + "This thread listens to network request from clients." + ); + + submitTo( + mThreads[Threads::Dispatcher], + [&dispatcher](const CancelToken& token) { + dispatcher.run(); + }, + "This thread dispatches the valid requests to sequence process" + ); + + LOG_INFO("Gateway loops submitted to workers"); + } + + void shutdown() { + LOG_INFO("Initiating Gateway shutdown..."); + mStopNetwork->store(true, std::memory_order_release); + LOG_INFO("Network stop signal sent, waiting for clean shutdown..."); + std::this_thread::sleep_for(std::chrono::seconds(2)); + Scheduler::shutdown(); + LOG_INFO("Gateway shutdown complete"); + } + }; +} \ No newline at end of file diff --git a/Gateway/main.cpp b/Gateway/main.cpp index 776d2ea..5cf14fa 100644 --- a/Gateway/main.cpp +++ b/Gateway/main.cpp @@ -1,63 +1,14 @@ -#include -#include -#include -#include -#include "messaging.h" -#include "enum.h" -#include "Exception.h" -#include "ipc/SharedMemory.h" -#include "Logger/Logger.h" -#include "Fix.h" +#include "Gateway.h" -// Producer -int main(int argc, char* argv[]) { - - int port = 8001; // Default port - // Parse command line argument for port - if (argc > 1) { - port = std::atoi(argv[1]); +int main() { + try { + Exchange::Gateway::Gateway gateway("Gateway"); + gateway.start(); } - - - - try{ - LOG_INFO("Launching producer..."); - - // // Initialize the queue - // Exchange::Ipc::Producer producer("queue"); // name: "queue" - // Exchange::Ipc::Msg::IpcMessage newOrder; - // newOrder.setMsgType(Exchange::Ipc::Msg::MsgType::NEW_ORDER); - // newOrder.addString(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_SYMBOL), "RIL"); - // newOrder.addUint64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_SIDE), static_cast(Exchange::Order::Side::BUY)); - // newOrder.addInt64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_PRICE), 123450); - // newOrder.addUint64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_QTY), 100); - // newOrder.addUint64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_CLIENT_ID), 42); - // newOrder.addUint64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_ORDER_ID), 1001); - // newOrder.addUint64(static_cast(Exchange::Ipc::Msg::FieldId::FIELD_TIF), static_cast(Exchange::Order::TIF::DAY)); - // newOrder.finalize(); - - // std::vector buf; - // newOrder.encode(buf); - - // bool success = producer.write(buf.data(), static_cast(buf.size())); - // if (success) { - // LOG_INFO("Message sent..."); - // printMessage(newOrder); - // } - // else { - // LOG_WARN("IPC Queue Full! Waiting..."); - // } - } catch (Engine::EngException& ex) { + LOG_ERROR("Exception caught:"); ex.log(); return 1; } - - // Start Threads - std::thread net_thread(Exchange::Gateway::networkLoop); - std::thread app_thread(Exchange::Gateway::consumerLoop); - // Wait forever - net_thread.join(); - app_thread.join(); return 0; -} +} \ No newline at end of file diff --git a/README.md b/README.md index 4e608e4..f752aef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Prototyping-IPC - +#Installation +```bash +chmod +x scripts/install-deps.sh +./scripts/install-deps.sh +``` mkdir build && cd build diff --git a/common/BlockingQueue/MutexBlockingQueue.h b/common/BlockingQueue/MutexBlockingQueue.h index 82dbe06..a657cf4 100644 --- a/common/BlockingQueue/MutexBlockingQueue.h +++ b/common/BlockingQueue/MutexBlockingQueue.h @@ -27,7 +27,6 @@ namespace Exchange::Core { ENG_THROW("Capacity must be > 0"); } } - // Disabling copy constructor MutexBlockingQueue (const MutexBlockingQueue &) = delete; // Disabling copy operation @@ -87,7 +86,7 @@ namespace Exchange::Core { // Wait until here is data in the ueue or closed is triggered m_NotEmptyCv.wait(lock, [&] { return (!m_Queue.empty() || m_Closed); - }); + }); // To check if we have data to pop after being notified coz of closed if (m_Queue.empty()) { @@ -101,7 +100,6 @@ namespace Exchange::Core { return true; } - T pop() override { T value; if (!pop(value)) { diff --git a/common/Scheduler/Scheduler.cpp b/common/Scheduler/Scheduler.cpp new file mode 100644 index 0000000..25e15d2 --- /dev/null +++ b/common/Scheduler/Scheduler.cpp @@ -0,0 +1,156 @@ +// +// Created by Vaasu Bisht on 19/10/25. +// + +#include "Scheduler.h" + +#include "Worker/Worker.h" +#include + +Scheduler::~Scheduler() +{ + shutdown(); +} + + +void Scheduler::createWorker(const std::string& id) +{ + std::unique_lock wlk(mLock); + + // Check if worker with id already exists. + if(mWorkers.contains(id)) + { + throw std::runtime_error("Worker: "+id+" already exists"); + } + mWorkers.emplace(id, std::make_unique(id)); +} + +void Scheduler::createWorkers(const std::string& prefix, const size_t cnt) +{ + { + std::shared_lock rlk(mLock); + if(mWorkers.size()) + { + mWorkers.clear(); + } + } + for(size_t i = 0 ; i < cnt ; i++) + { + createWorker(prefix+"_"+std::to_string(i)); + } +} + +void Scheduler::start() +{ + std::unique_lock wlk(mLock); + for (auto& [_, worker] : mWorkers) + { + worker->start(); + } +} + +void Scheduler::shutdown() +{ + { + std::unique_lock wlk(mLock); + if(mShutdown) + { + return; + } + mShutdown = true; + for(auto& [_,worker]:mWorkers) + { + worker->postStop(); + } + } + for(auto& [_,worker]:mWorkers) + { + worker->join(); + } + std::unique_lock wlk(mLock); + mWorkers.clear(); +} + +uint64_t Scheduler::submitTo(const std::string& wId, const TaskFn& func, const std::string& desc) +{ + const auto t = makeTask(func,desc); + auto* w = getWorker(wId); + w->postTask(t); + return t.id; +} + +Task Scheduler::makeTask(const TaskFn& fn, const std::string& desc) +{ + Task t; + t.id = nextTaskId(); + t.func = fn; + t.token = CancelToken(); + t.desc = desc; + return t; +} + +template +auto Scheduler::submitToWithFuture(const std::string& wid, F&& f, Args&&... args) + -> std::future> +{ + using R = std::invoke_result_t; // Result type + auto pkg = std::make_shared>(); + auto fut = pkg->get_future(); + + auto wrapper = [pkg, fn = std::forward(f), tup = std::make_tuple(std::forward(args)...)] + (const CancelToken& token) mutable + { + // Optionally check token before running + if(token.isCancelled()) + { + // For promise types, set an exception or set_defaults + try + { + throw std::runtime_error("Task cancelled"); + } + catch (...) + { + pkg->set_exception(std::current_exception()); + } + return; + } + try + { + if constexpr (std::is_void_v) + { + std::apply(fn, tup); + pkg->set_value(); + } + else + { + R r = std::apply(fn, tup); + pkg->set_value(std::move(r)); + } + } + catch (...) + { + pkg->set_exceeption(std::current_exception()); + } + }; + auto t = makeTask(std::move(wrapper),"future_task"); + getWorker(wid)->postTask(t); + return fut; +} + +std::vector Scheduler::workerIds() const +{ + std::vector ids; + std::shared_lock rlk(mLock); + ids.reserve(mWorkers.size()); + for(auto const& [id,_] : mWorkers) + { + ids.push_back(id); + } + return ids; +} + +bool Scheduler::hasWorker(const std::string& id) const +{ + std::shared_lock rlk(mLock); + return mWorkers.count(id) != 0 ; +} \ No newline at end of file diff --git a/common/Scheduler/Scheduler.h b/common/Scheduler/Scheduler.h new file mode 100644 index 0000000..cc80cbf --- /dev/null +++ b/common/Scheduler/Scheduler.h @@ -0,0 +1,146 @@ +// +// Created by Vaasu Bisht on 19/10/25. +// + +#pragma once + +#ifndef SCHEDULER_H +#define SCHEDULER_H + +#include +#include +#include +#include +#include "Worker/Task.h" +#include "Worker/Worker.h" + +struct Worker; +class Order; + + +namespace Msg +{ + struct AddBook {std::string symbol;}; + struct NewOrder {std::string symbol; Order* order;}; + struct ControlStop {}; + using Msg = std::variant; + } + +/** + * @class Scheduler + * @brief Lightweight multithreaded task scheduler that manages multiple worker threads + * and task queues. + * + * It provides a framework for async task execution across a configurable number of worker threads. + * Each worker maintains its own task queue and continuously processes task in FIFO order. Task can + * be submitted to specific workers using `submitTo()` or with a returnable `std::future` via + * `submitToWithFuture()`. + */ +class Scheduler +{ + // using WorkerMap = std::map>; + + // <====== Data Members ======> + std::map> mWorkers; ///< Holds all the workers currently active by their id + mutable std::shared_mutex mLock; ///< Mutex for WorkerMap + bool mShutdown{false}; ///> Indicates that all workers are shutdown + +public: + /* @brief Default constructor. Initializes an empty scheduler */ + Scheduler() = default; + + /* @brief Delete copy constructor to prevent copying of v instances. */ + Scheduler(const Scheduler&) = delete; + + /* @brief Delete copy constructor to prevent copying of v instances. */ + Scheduler& operator=(const Scheduler&) = delete; + + /* @brief Destructor that ensures all workers are stopped and joined */ + virtual ~Scheduler(); + + /** + * @brief Start all the workers + */ + void start(); + + /** + * @brief Gracefully stops all workers and joins their threads. + */ + void shutdown(); + + /** + * @brief Creates and reserves a single worker with a unique identifier. + * @param id The string identifier for the worker (e.g., "worker_1") + * @throws std::runtime_error If a worker with the same id already exists. + */ + void createWorker(const std::string& id); + + /** + * @brief Reserves multiple workers using a naming prefix. + * @param prefix The prefix for worker's names. + * @param cnt The number of workers to create. + * + * @example + * prefix = "workers", count = 3 -> creates 'worker_0', 'worker_1', 'worker_2' + */ + void createWorkers(const std::string& prefix, size_t cnt); + + /** + * @brief Submits a fire-and-forget task to a worker + * @param wId Unique identified of workers + * @param func Callable which the task will call + * @param desc + * @return Task Id + */ + uint64_t submitTo(const std::string& wId, const TaskFn& func, const std::string& desc = ""); + + /** + * Helper method to create a task. + * @param fn Callable which will be called during execution. + * @param desc Description of task (optional) + * @return + */ + static Task makeTask(const TaskFn& fn, const std::string& desc = ""); + + /** + * @brief Locates the worker pointer + * @param id Identifier of thread needed to be located + * @return Worker pointer3 + */ + Worker* getWorker(const std::string& id) + { + std::shared_lock rlk(mLock); // read only lock + const auto it = mWorkers.find(id); + if(it==mWorkers.end()) + { + throw std::runtime_error("Worker not found: " + id); + } + return it->second.get(); + } + + /** + * + * @tparam F The callable type (lambda, function, or functor). + * @tparam Args The types of arguments to pass to the callable. + * @param wid The identifier of the worker thread that should run the task + * @param f The callable to execute asynchronously. + * @param args Arguments to pass to the callable. + * @return A future that can be used to retrieve the callable's + * return value. + */ + template + auto submitToWithFuture(const std::string& wid, F&& f, Args&&... args) + ->std::future>; + + /** + * @brief Get all active worker's id. + */ + std::vector workerIds() const; + + bool hasWorker(const std::string& id) const; + +}; + + + +#endif //SCHEDULER_H \ No newline at end of file diff --git a/common/Scheduler/Worker/Task.h b/common/Scheduler/Worker/Task.h new file mode 100644 index 0000000..e8a2b3b --- /dev/null +++ b/common/Scheduler/Worker/Task.h @@ -0,0 +1,67 @@ +// +// Created by Vaasu Bisht on 19/10/25. +// File contains: CancelToken, Task +// + +#pragma once + +#ifndef TASK_H +#define TASK_H +#include +#include +#include + +// <================================ Cancel Token ================================> + +struct CancelToken +{ + std::shared_ptr> cancelled = std::make_shared>(false); + bool isCancelled() const noexcept + { + return cancelled && cancelled->load(std::memory_order_acquire); + } + + void cancel() const noexcept + { + if(cancelled) + { + cancelled->store(true,std::memory_order_relaxed); + } + } +}; + + +// <================================ Task ================================> + + +using TaskFn = std::function; + +/** + * @struct Task + * @brief Task is a wrapper of all callable (function, lambda, functor). It is a functor + * itself. + */ +struct Task +{ + uint64_t id; // unique identification + TaskFn func; + CancelToken token; + std::string desc; + + void operator()() const + { + func(token); + } +}; + +/** + * @brief Global unique task id generator + * @return unique task id + */ +inline uint64_t nextTaskId() +{ + static std::atomic i{1}; + return i.fetch_add(1, std::memory_order_relaxed); +} + +#endif //TASK_H \ No newline at end of file diff --git a/common/Scheduler/Worker/Worker.cpp b/common/Scheduler/Worker/Worker.cpp new file mode 100644 index 0000000..3383d53 --- /dev/null +++ b/common/Scheduler/Worker/Worker.cpp @@ -0,0 +1,114 @@ +// +// Created by Vaasu Bisht on 19/10/25. +// + +#include "Worker.h" +#include +Worker::Worker(const std::string& id):mId(std::move(id)), mStop(false){} + +void Worker::start() +{ + std::lock_guard lk(mThreadMutex); + if(mThread.joinable()) + { + return; + } + mThread = std::thread([this] + { + run(); + }); +} + + +void Worker::shutdown() +{ + if(mStop) + { + return; + } + mStop = true; +} + +void Worker::run() +{ + while(true) + { + Task t; + { + std::unique_lock lock(mQueueMutex); + + // Lock is released until there is task in queue to be processed and thread goes to waiting state + // until either queue has a task or stop signal is received. + mCv.wait(lock, [this] + { + return mStop || !mQueue.empty(); // Any one being true means gaining lock again and going forward. + }); + + // If stop signal is received and there is something to read exit the loop + if(mQueue.empty() && mStop) + { + return; + } + + // Checking if queue is empty + if(mQueue.empty()) continue; + + // Adding task in queue + t = std::move(mQueue.front()); + mQueue.pop(); + mRunningTasks.insert(t.id); + } + try + { + if(!t.token.isCancelled()) + { + t(); + } + else + { + // Skipped due to cancel + } + } + catch(const std::exception& e) + { + std::cerr<<"[Worker]: "< lock(mQueueMutex); + mQueue.push((t)); + mPendingTasks.insert((t.id)); + } + mCv.notify_one(); +} + +void Worker::postStop() +{ + { + std::lock_guard lock(mQueueMutex); + mStop = true; + } + mCv.notify_one(); +} + +void Worker::join() noexcept +{ + std::thread local; + { + std::lock_guard lk(mThreadMutex); + if(!mThread.joinable()) + { + return; + } + local = std::move(mThread); + } + if(local.joinable()) + { + local.join(); + } +} \ No newline at end of file diff --git a/common/Scheduler/Worker/Worker.h b/common/Scheduler/Worker/Worker.h new file mode 100644 index 0000000..537fa88 --- /dev/null +++ b/common/Scheduler/Worker/Worker.h @@ -0,0 +1,102 @@ +// +// Created by Vaasu Bisht on 19/10/25. +// + +#pragma once + +#ifndef WORKER_H +#define WORKER_H + +#include +#include +#include + +#include "Task.h" +#include "../Scheduler.h" + +struct Task; + +/** + * @struct Worker + * @brief Internal class representing a single worker thread. This class is owner + * of thread. + * + * Each worker has its own task queue and continuously processes tasks until explicitly + * stopped. SSynchronization is managed via mutex and conditional variables. + * + * @details + * `threadMutex` protects ownership changes of the std::thread object (start/join/move/destruction). + * Without this, there can be data races — e.g., one thread calling start() while another calls join(). + */ +struct Worker { + using Id = std::string; + using TokenMap = std::unordered_map>>; + using UnorderedTaskIdSet = std::unordered_set; + std::string mId; + std::queue mQueue; + std::mutex mQueueMutex; + std::mutex mThreadMutex; + std::condition_variable mCv; + std::thread mThread; + TokenMap mTokenMap; /// > Tasks which are cancelable, thought a cancel token. + UnorderedTaskIdSet mPendingTasks; /// > Tasks that are pending (queued but not started). + UnorderedTaskIdSet mRunningTasks; /// > Tasks that are currently running. + + + bool mStop; + /** @brief Constructor */ + explicit Worker(const std::string& id); + + /** + * @brief Responsible for creating and launching work's dedicated thread + * + * @details + * Has a dedicated thread-mutex as multiple threads in the system could + * potentially call start()/shutdown() on the same worker concurrently. + */ + void start(); + void shutdown(); + + /** + * @brief Worker's main loop. + * + * Continuously waits for a task to be enqueued and executes them one by one. The loop exists when + * `mStop` is set and all pending tasks are processed. + * @details + * Conditional variable is used so thread avoids busy-waiting. Thread sleep until there is task + * available to be executed. + * Scheduler APIs like postTask(), tryRemoveOrCancel() etc. writes into the worker's queue while run() + * reads from it. So queue is used by multiple thread that's why there is need of mQueueMutex. + */ + void run(); + + /** + * @brief Add new work (task) for the worker thread + * + * Locks the worker's task queue, add a Task object, and then releases the lock and wakes up the + * sleeping thread(worker). The worker thread wakes up, pop the new task and runs it. + * @param t + */ + void postTask(const Task& t); + + /** + * @brief Signals the worker to exit gracefully. + * + * Request that the worker thread finishes all pending work and then terminates clearly. + */ + void postStop(); + + /** + * @brief + * To optimize it's better to release the lock before actually calling .join() as std::thread::join() + * is a blocking call — it can take a long time (the worker might still be finishing its run loop). Holding + * a mutex while doing a blocking operation — that would block other threads (e.g. start(), shutdown()) + * from making progress. After std::move, this->thread becomes empty (non-joinable). The lock is then released. + * Other code can now safely call start() or inspect state without deadlock risk. + */ + void join() noexcept; +}; + + + +#endif //WORKER_H \ No newline at end of file diff --git a/common/xml/XMLNode.h b/common/xml/XMLNode.h new file mode 100644 index 0000000..db52464 --- /dev/null +++ b/common/xml/XMLNode.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include "tinyxml2.h" +#include "String.h" +#include "Exception.h" + +namespace Exchange::Core { + + class XMLNode { + public: + explicit XMLNode(const tinyxml2::XMLElement* element = nullptr) + : mElement(element) {} + + const tinyxml2::XMLElement* mElement; + bool isValid() const { + return mElement != nullptr; + } + + XMLNode getChild(const String& name) const { + if (!isValid()) { + ENG_THROW("Attempt to access child of null XmlNode"); + } + + const tinyxml2::XMLElement* child = mElement->FirstChildElement(name); + if (!child) { + ENG_THROW(String("Missing required XML element: ") + name); + } + + return XMLNode(child); + } + + String get() const { + if (!mElement || !mElement->GetText()) { + ENG_THROW("XML element has no text"); + } + return mElement->GetText(); + } + }; +} \ No newline at end of file diff --git a/common/xml/XMLReader.h b/common/xml/XMLReader.h new file mode 100644 index 0000000..73a0bcd --- /dev/null +++ b/common/xml/XMLReader.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include "tinyxml2.h" +#include "XMLNode.h" +#include "String.h" + +namespace Exchange::Core { + + class XMLReader { + const Core::String gExchange = "Exchange"; + + tinyxml2::XMLDocument mDoc; + const tinyxml2::XMLElement* mRoot; + public: + explicit XMLReader(const String& fileName) { + if (mDoc.LoadFile(fileName) != tinyxml2::XML_SUCCESS) { + ENG_THROW("Failed to load XML file: %ls", fileName.get()); + } + + mRoot = mDoc.FirstChildElement(gExchange); + if (!mRoot) { + ENG_THROW("Missing root node"); + } + } + + auto getNode(String name) { + auto node = mRoot->FirstChildElement(name); + if (!node) { + ENG_THROW("Missing <%ls> node", name); + } + return node; + } + }; +} // namespace Exchange::Core \ No newline at end of file diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..5c784d1 --- /dev/null +++ b/config.xml @@ -0,0 +1,27 @@ + + + + + 9000 + + + + + 4096 + + + + 100 + 100 + + + + + + + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..3683c06 --- /dev/null +++ b/main.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import socket + +def send_fix_message(host='localhost', port=9000): + SOH = '\x01' + + # FIX Logon Message + logon_msg = f"8=FIX.4.2{SOH}35=A{SOH}10=000{SOH}" + + # FIX New Order Single Message + order_msg = f"8=FIX.4.2{SOH}35=D{SOH}55=AAPL{SOH}54=1{SOH}44=150.50{SOH}38=100{SOH}10=000{SOH}" + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + print(f"Connected to {host}:{port}") + + # Send logon + sock.sendall(logon_msg.encode()) + print("Sent Logon message") + + # Send order + sock.sendall(order_msg.encode()) + print("Sent Order message") + + # Keep connection open briefly + import time + time.sleep(1) + + except Exception as e: + print(f"Error: {e}") + finally: + sock.close() + print("Connection closed") + +if __name__ == "__main__": + send_fix_message() \ No newline at end of file diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh new file mode 100755 index 0000000..0d5439d --- /dev/null +++ b/scripts/install-deps.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e + +OS="$(uname)" + +if [[ "$OS" == "Darwin" ]]; then + # macOS (Homebrew) + if command -v brew >/dev/null 2>&1; then + brew install tinyxml2 + else + echo "Homebrew not found. Install Homebrew or use CMake FetchContent." + exit 1 + fi +else + # assume Debian/Ubuntu + sudo apt-get update + sudo apt-get install -y libtinyxml2-dev +fi \ No newline at end of file diff --git a/run_tests.sh b/scripts/run-tests.sh similarity index 100% rename from run_tests.sh rename to scripts/run-tests.sh diff --git a/tests/test_gateway.cpp b/tests/test_gateway.cpp index 21cc1cc..b136aee 100644 --- a/tests/test_gateway.cpp +++ b/tests/test_gateway.cpp @@ -8,6 +8,7 @@ #include #include #include +#include // ANSI color codes #define RESET "\033[0m" @@ -362,8 +363,12 @@ int main() { std::cout << MAGENTA << " Gateway Network & FIX Protocol Tests" << RESET << std::endl; std::cout << MAGENTA << "========================================" << RESET << "\n" << std::endl; + // Start the Gateway in a background process + log("INFO", "Starting Gateway process...", YELLOW); + system("cd /workspaces/Stock-Exchange/build && ./Gateway > /tmp/gateway.log 2>&1 &"); + log("INFO", "Waiting for gateway to start...", YELLOW); - std::this_thread::sleep_for(std::chrono::seconds(1)); + std::this_thread::sleep_for(std::chrono::seconds(2)); int passed = 0; int total = 6;