My OCaml code can pass the multiple pings test but not the first one

I’m stuck on challenge #zu2. I refactored from regular Unix net to using Eio. It passes the ZU2 tests just fine but it fails the WY1 test that runs right after it.

From the logs, it looks like it is running ./your_program.sh twice.. My app doesn’t quit after the ZU2 tests are run, so I’m getting:

[tester::#WY1] $ ./your_program.sh
[tester::#WY1] client-1: $ redis-cli PING
[your_program] +server: Running server
[tester::#WY1] Received: "" (no content received)
[tester::#WY1]            ^ error
[tester::#WY1] Error: Expected start of a new RESP2 value (either +, -, :, $ or *)
[tester::#WY1] Test failed (try setting 'debug: true' in your codecrafters.yml to see more details)

Can someone familiar with Eio for Ocaml take a look at my code and see where I might not be cleaning up?

open Eio.Std

(* Prefix all trace output with "server: " *)
let traceln fmt = traceln ("server: " ^^ fmt)

module Read = Eio.Buf_read

(* Read one line from [client] and respond with "OK". *)
let rec handle_client flow addr =
  traceln "Reading line from %a" Eio.Net.Sockaddr.pp addr;
  (* We use a buffered reader because we may need to combine multiple reads
     to get a single line (or we may get multiple lines in a single read,
     although here we only use the first one). *)
  let from_client = Read.of_flow flow ~max_size:1024 in
  let line = Read.line from_client in
  traceln "Received: %S" line;
  Eio.Flow.copy_string "+PONG\r\n" flow;

  handle_client flow addr

(* Accept incoming client connections on [socket].
   We can handle multiple clients at the same time.
   Never returns (but can be cancelled). *)

let run socket =
    traceln "Running server";
    Eio.Net.run_server socket handle_client
      ~on_error:(traceln "Error handling connection: %a" Fmt.exn)
      ~max_connections:1000

Here’s the log for the passing ZU2 test in case it helps:

tester::#ZU2] Running tests for Stage #ZU2 (Handle concurrent clients)
[tester::#ZU2] $ ./your_program.sh
[your_program] +server: Running server
[your_program] +server: Reading line from tcp:127.0.0.1:36966
[tester::#ZU2] client-1: $ redis-cli PING
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36966
[tester::#ZU2] Received "PONG"
[tester::#ZU2] client-2: $ redis-cli PING
[your_program] +server: Reading line from tcp:127.0.0.1:36972
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36972
[tester::#ZU2] Received "PONG"
[tester::#ZU2] client-1: > PING
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36966
[tester::#ZU2] Received "PONG"
[tester::#ZU2] client-1: > PING
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36966
[tester::#ZU2] Received "PONG"
[tester::#ZU2] client-2: > PING
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36972
[tester::#ZU2] Received "PONG"
[your_program] +server: Error handling connection: End_of_file
[tester::#ZU2] client-3: $ redis-cli PING
[your_program] +server: Reading line from tcp:127.0.0.1:36988
[your_program] +server: Received: "*1"
[your_program] +server: Reading line from tcp:127.0.0.1:36988
[tester::#ZU2] Received "PONG"
[tester::#ZU2] Test passed.

From the logs it almost looks like it tried to get data back from the server before it started the second one. Maybe it’s a race condition? The code appears to conform to Redis when I run it locally.

Hey @autodidaddict, I ran your code and it passed the tests locally. We’ll take a closer look on our end to see what might be going on.

Thanks for looking into this! I’m at my wit’s end with it. I can use the redis-cli to do everything I need, and ZU2 passes, but I don’t know why the other fails.

@autodidaddict We’ve identified the root cause of the issue, but haven’t been able to get your code working just yet.

The main issue seems to be that the program isn’t fully releasing the listening port (6379) after it exits:

In contrast, when a stage passes, the port was cleanly released by the previous stage:

Additionally, some connections from a previous stage can remain in the ESTABLISHED state, which could be preventing the listening port from being released properly:

I’ve sent you a repo link via DM with can reproduce the above logs.

Is there some hook in Eio that I am not using that would properly clean up and shut down the socket when it exits? I know as much about Eio as I do about OCaml (which is pretty slim). I’ve looked at the Eio net example in the Eio github repo and it doesn’t have any kind of cleanup code…but then it also assumes that it will exit immediately.

The code from the DM doesn’t compile for me, so I changed listening_socket.close() to Eio.Net.close listening_socket; to fix this and I can now reproduce the same failure and logs.

Sorry about the compile error! I pushed a new commit to the repo that updates the line to Eio.Net.close listening_socket;

I know as much about Eio as I do about OCaml (which is pretty slim).

Same here. I hadn’t heard of Eio until I saw your post. :sweat_smile:

I tried quite a few things suggested by AI, but none were able to clean up properly.

So far, none of our OCaml users who passed this stage were using Eio, which is relatively new compared to Lwt and Async.

I don’t want to accept defeat! :smile: I feel like there has to be something simple that I’m missing in my code that will make the tests pass. I wrote a whole blog post on how entirely unhelpful AI was in this exercise :slight_smile:

2 Likes

Closing this thread due to inactivity. If you still need assistance, feel free to reopen or start a new discussion!