Skip to content

Commit dd9bf1e

Browse files
authored
Merge pull request #31 from Ataba29/fragmentation_integration_testing
Fragmentation integration testing
2 parents 4e26e86 + 5ced32a commit dd9bf1e

14 files changed

Lines changed: 304 additions & 36 deletions

File tree

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,42 @@ jobs:
4242

4343
- name: Test
4444
run: ctest --test-dir build --output-on-failure
45+
46+
integration-test:
47+
name: Integration Tests (Docker)
48+
runs-on: ubuntu-24.04
49+
needs: build-linux
50+
steps:
51+
- uses: actions/checkout@v7
52+
53+
- name: Set up Python
54+
uses: actions/setup-python@v5
55+
with:
56+
python-version: "3.12"
57+
58+
- name: Install test dependencies
59+
run: pip install -r src/Tests/integration/requirements.txt
60+
61+
- name: Build Docker image
62+
run: docker build -t byteforge:ci .
63+
64+
- name: Run ByteForge container
65+
run: docker run -d --name byteforge -p 6625:6625 byteforge:ci
66+
67+
- name: Wait for server to be ready
68+
run: |
69+
for i in {1..30}; do
70+
(echo > /dev/tcp/127.0.0.1/6625) 2>/dev/null && break
71+
sleep 1
72+
done
73+
74+
- name: Run integration tests
75+
run: pytest src/Tests/integration -v
76+
77+
- name: Dump container logs on failure
78+
if: failure()
79+
run: docker logs byteforge
80+
81+
- name: Stop container
82+
if: always()
83+
run: docker rm -f byteforge

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ CMakeFiles/
88
CMakeScripts/
99
cmake_install.cmake
1010
Makefile
11+
.venv/
12+
__pycache__/
1113

1214
# ==========================================
1315
# Compiled Executables and Libraries and Logs

src/Networking/EpollEventLoop.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ EpollEventLoop::~EpollEventLoop()
1818
void EpollEventLoop::add(SocketType sock)
1919
{
2020
epoll_event ev{};
21-
ev.events = EPOLLIN; // watch for "readable" (level-triggered by default)
21+
ev.events = EPOLLIN | EPOLLONESHOT; // watch for "readable", (intentionally "mutes" the socket after the first notification)
2222
ev.data.fd = sock;
2323

2424
epoll_ctl(epollFd, EPOLL_CTL_ADD, sock, &ev);
@@ -67,4 +67,13 @@ int EpollEventLoop::wait(std::vector<EventLoopEntry> &out)
6767
return numReady;
6868
}
6969

70+
bool EpollEventLoop::rearm(SocketType sock)
71+
{
72+
epoll_event ev{};
73+
ev.events = EPOLLIN | EPOLLONESHOT;
74+
ev.data.fd = sock;
75+
76+
return epoll_ctl(epollFd, EPOLL_CTL_MOD, sock, &ev) == 0;
77+
}
78+
7079
#endif //_WIN32

src/Networking/EpollEventLoop.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class EpollEventLoop : public IEventLoop
3333
void add(SocketType sock) override;
3434
void remove(SocketType sock) override;
3535
int wait(std::vector<EventLoopEntry> &out) override;
36+
bool rearm(SocketType sock) override;
3637

3738
private:
3839
/// File descriptor for the epoll instance itself.

src/Networking/EventLoop.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ class IEventLoop
6565
* nothing ready, or -1 on error.
6666
*/
6767
virtual int wait(std::vector<EventLoopEntry> &out) = 0;
68+
69+
// Explained inside the Server.h
70+
virtual bool rearm(SocketType sock) = 0;
6871
};
6972

7073
#endif // KV_DATABASE_EVENTLOOP_H

src/Networking/IocpEventLoop.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,17 @@ int IocpEventLoop::wait(std::vector<EventLoopEntry> &out)
8282
// recv() next; if THAT returns 0, that's how a graceful close is
8383
// detected - same pattern as epoll's EPOLLIN + recv()==0 on Linux.
8484
out.push_back(EventLoopEntry{sock, IOEvent::Readable});
85-
armRead(sock); // re-arm so we're notified again for the next batch of data
85+
// We no longer auto-rearm here. The worker thread is now responsible
86+
// for calling rearm() once it finishes processing fragmentation.
8687
}
8788

8889
return 1;
8990
}
9091

92+
bool IocpEventLoop::rearm(SocketType sock)
93+
{
94+
armRead(sock);
95+
return true; // WSARecv failures handle themselves asynchronously in wait()
96+
}
97+
9198
#endif //_WIN32

src/Networking/IocpEventLoop.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class IocpEventLoop : public IEventLoop
4949
void add(SocketType sock) override;
5050
void remove(SocketType sock) override;
5151
int wait(std::vector<EventLoopEntry> &out) override;
52+
bool rearm(SocketType sock) override;
5253

5354
private:
5455
/// Posts (or re-posts) the zero-byte WSARecv that arms readiness notification for a socket.

src/Server/Server.cpp

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ void Server::acceptClients()
125125
CloseSocket(AcceptSocket);
126126
continue;
127127
}
128-
connections[AcceptSocket] = Connection{AcceptSocket, active_key};
128+
connections[AcceptSocket] = std::make_shared<Connection>(AcceptSocket, active_key);
129129
eventLoop->add(AcceptSocket);
130130
std::cout << "[SERVER] Client connected and registered!\n";
131131
}
@@ -182,27 +182,14 @@ void Server::runEventLoop()
182182
if (entry.event == IOEvent::Readable)
183183
{
184184
auto it = connections.find(entry.socket);
185-
if (it == connections.end())
186-
continue; // already cleaned up
187-
188-
{
189-
std::lock_guard<std::mutex> lock(busyMutex);
190-
// A job for this socket is already queued or running -
191-
// skip this notification. Level-triggered epoll will
192-
// notify us again next wait() if data is still unread.
193-
if (busySockets.count(entry.socket))
194-
continue;
195-
busySockets.insert(entry.socket);
196-
}
197-
198-
Connection conn = it->second; // small struct, cheap to copy
199-
tpool.acceptJob([this, conn]()
200-
{
201-
messageHandler(conn.socket, conn.sessionKey);
202-
std::lock_guard<std::mutex> lock(busyMutex);
203-
busySockets.erase(conn.socket); });
204-
}
205-
else // HangUp or Error
185+
if (it == connections.end()) continue;
186+
187+
std::shared_ptr<Connection> user_connection = it->second; // No Copy
188+
// wont fire again until we re-arm it.
189+
tpool.acceptJob([this, conn = std::move(user_connection)] {
190+
messageHandler(conn);
191+
});
192+
} else // HangUp or Error
206193
{
207194
closeConnection(entry.socket);
208195
}
@@ -217,21 +204,22 @@ void Server::closeConnection(SocketType sock)
217204
auto it = connections.find(sock);
218205
if (it != connections.end())
219206
{
220-
userSessionManager.remove_session(it->second.sessionKey);
207+
userSessionManager.remove_session(it->second->sessionKey);
221208
connections.erase(it);
222209
}
223210

224211
CloseSocket(sock);
225212
}
226213

227-
void Server::messageHandler(SocketType clientSocket, const SessionKey &sessionKey)
214+
void Server::messageHandler(std::shared_ptr<Connection> clientConnection)
228215
{
229216
std::cout << "[CLIENT] Handling client message\n";
230217

231-
char buffer[1024];
218+
char tempBuffer[1024];
219+
SocketType clientSocket = clientConnection->socket;
232220

233221
// On Linux, the buffer is safely passed to standard recv
234-
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
222+
int bytesReceived = recv(clientSocket, tempBuffer, sizeof(tempBuffer), 0);
235223

236224
if (bytesReceived == 0)
237225
{
@@ -247,19 +235,37 @@ void Server::messageHandler(SocketType clientSocket, const SessionKey &sessionKe
247235
// Nothing to read right now - a different job for this socket
248236
// already drained it, or epoll notified us before this job
249237
// got scheduled. Not an error, just nothing to do.
238+
this->rearmSocket(clientSocket);
250239
return;
251240
}
252241
std::cout << "[CLIENT] recv error, disconnecting\n";
253242
closeConnection(clientSocket);
254243
return;
255244
}
256-
userSessionManager.update_activity(sessionKey);
245+
246+
clientConnection->commandBuffer.append(tempBuffer, bytesReceived);
247+
248+
if (clientConnection->commandBuffer.length() > this->MAX_COMMAND_BUFFER_LENGTH) {
249+
std::cout << "[CLIENT] Client is abusing the command buffer disconnecting them\n";
250+
closeConnection(clientSocket);
251+
return;
252+
}
253+
254+
if (clientConnection->commandBuffer.find('\n') == std::string::npos) {
255+
this->rearmSocket(clientSocket);
256+
return;
257+
}
258+
259+
260+
261+
userSessionManager.update_activity(clientConnection->sessionKey);
257262

258263
std::cout << "[CLIENT] Received " << bytesReceived << " bytes\n";
259-
std::string message(buffer, bytesReceived);
264+
std::string message = clientConnection->commandBuffer;
260265
std::cout << "[CLIENT] Message: " << message << "\n";
261266
std::istringstream iss(message);
262267
std::string command, key, value;
268+
clientConnection->commandBuffer.clear();
263269

264270
iss >> command;
265271
iss >> key;
@@ -277,6 +283,7 @@ void Server::messageHandler(SocketType clientSocket, const SessionKey &sessionKe
277283
{
278284
std::string response = "Empty Value Recieved, Try again\n";
279285
send(clientSocket, response.c_str(), response.length(), 0);
286+
this->rearmSocket(clientSocket);
280287
return;
281288
}
282289

@@ -317,6 +324,18 @@ void Server::messageHandler(SocketType clientSocket, const SessionKey &sessionKe
317324
std::string response = "No command was received\n";
318325
send(clientSocket, response.c_str(), response.length(), 0);
319326
}
327+
328+
//Re-enable epoll notifications for the next command from this client
329+
this->rearmSocket(clientSocket);
330+
}
331+
332+
void Server::rearmSocket(SocketType clientSocket)
333+
{
334+
if (!eventLoop->rearm(clientSocket))
335+
{
336+
std::cout << "[SERVER] Failed to re-arm socket " << clientSocket << ", closing.\n";
337+
closeConnection(clientSocket);
338+
}
320339
}
321340

322341
void Server::onSessionExpired(SocketType sock)

src/Server/Server.h

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@ class Server
3737
RateLimiter rt; /** Server owns an instance of RateLimter class */
3838
UserSessionManager userSessionManager; /** Managing User Sessions */
3939
UserSessionBackgroundWorker user_session_background_worker; /** Background worker that sweeps expired sessions*/
40-
std::unordered_map<SocketType, Connection> connections; /** Client connections, keyed by socket */
40+
std::unordered_map<SocketType, std::shared_ptr<Connection>> connections; /** Client connections, keyed by socket */
4141
std::unique_ptr<IEventLoop> eventLoop; /** Watches all client sockets for readiness */
4242
std::thread eventLoopThread; /** Thread that runs runEventLoop() */
43-
std::mutex busyMutex; /** Guards busySockets */
44-
std::unordered_set<SocketType> busySockets; /** Sockets with a recv job already queued/running */
4543
std::mutex expiredMutex; /** Mutex to guard the expiredSockets vector */
4644
std::vector<SocketType> expiredSockets; /** Notifys the event loop of connections to remove */
4745

@@ -89,16 +87,29 @@ class Server
8987
/**
9088
* @brief Handles one ready-to-read event for a client: one recv() call,
9189
* command parsing, and response.
92-
* @param clientSocket The socket that has data available.
93-
* @param sessionKey The session tied to this client.
90+
* @param userConnection The user connection
91+
*
9492
*/
95-
void messageHandler(SocketType clientSocket, const SessionKey &sessionKey);
93+
void messageHandler(std::shared_ptr<Connection> userConnection);
9694

9795
/**
9896
* @brief Adds sockets that are expired into the expired vector to be removed later by eventloop
9997
* @param sock which is the client socket that is to be removed
10098
*/
10199
void onSessionExpired(SocketType sock);
100+
101+
/**
102+
* What it is: An explicit call (epoll_ctl with EPOLL_CTL_MOD) executed when a thread finishes its work.
103+
* What it does: Unmutes the socket so epoll can start listening for network activity again.
104+
* Why we use it: Because EPOLLONESHOT completely mutes the socket, it will stay dead forever unless re-armed.
105+
* Every exit path in your thread—whether it finished a full message or is waiting for more bytes (fragmentation)—must call rearm().
106+
*
107+
* @param clientSocket
108+
*/
109+
void rearmSocket(SocketType clientSocket);
110+
111+
112+
uint16_t MAX_COMMAND_BUFFER_LENGTH = 2024;
102113
};
103114

104115
#endif

src/Tests/integration/client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import socket
2+
import time
3+
4+
5+
class ByteForgeClient:
6+
def __init__(self, host="127.0.0.1", port=6625, timeout=5.0,
7+
max_retries=5, retry_delay=0.25):
8+
self.host = host
9+
self.port = port
10+
self.timeout = timeout
11+
self.max_retries = max_retries
12+
self.retry_delay = retry_delay
13+
self.sock = self._connect_with_retry()
14+
15+
def _connect_with_retry(self):
16+
last_error = None
17+
for _ in range(self.max_retries):
18+
try:
19+
return socket.create_connection((self.host, self.port), timeout=self.timeout)
20+
except (ConnectionRefusedError, ConnectionResetError,
21+
ConnectionAbortedError, OSError) as e:
22+
last_error = e
23+
time.sleep(self.retry_delay)
24+
raise ConnectionError(
25+
f"Failed to connect to {self.host}:{self.port} after {self.max_retries} attempts"
26+
) from last_error
27+
28+
def _send(self, command: str) -> str:
29+
last_error = None
30+
for _ in range(self.max_retries):
31+
try:
32+
self.sock.sendall((command + "\n").encode())
33+
return self._recv_line()
34+
except (ConnectionResetError, ConnectionAbortedError,
35+
BrokenPipeError, OSError) as e:
36+
last_error = e
37+
try:
38+
self.sock.close()
39+
except OSError:
40+
pass
41+
time.sleep(self.retry_delay)
42+
self.sock = self._connect_with_retry()
43+
raise ConnectionError(
44+
f"Failed to send command after {self.max_retries} attempts"
45+
) from last_error
46+
47+
def _recv_line(self) -> str:
48+
data = b""
49+
while not data.endswith(b"\n"):
50+
chunk = self.sock.recv(1024)
51+
if not chunk:
52+
break
53+
data += chunk
54+
return data.decode().strip()
55+
56+
def insert(self, key: str, value: str) -> str:
57+
return self._send(f"INSERT {key} {value}")
58+
59+
def get(self, key: str) -> str:
60+
return self._send(f"GET {key}")
61+
62+
def delete(self, key: str) -> str:
63+
return self._send(f"DELETE {key}")
64+
65+
def close(self):
66+
self.sock.close()

0 commit comments

Comments
 (0)