canonical / dqlite

Embeddable, replicated and fault-tolerant SQL engine.
https://dqlite.io
Other
3.8k stars 214 forks source link

Not Leader failure response #531

Closed halexh closed 11 months ago

halexh commented 11 months ago

I have created a simple c app utilizing the dqlite library, called dd. Within it, I have started a dqlite_server and implemented the wire protocol to successfully communicate with it (utilizing a TCP socket). I am trying to take it to the next level, and demonstrate the distributed database portion of it. In order to do that, I have created two instances of this app, that only differ in the following ways:

My expectation is that Instance B should communicate with its local dqserver (port 8082) via wire protocol. But any attempt to do so results in a "not leader" response Failure Message. Communicating with instance A's dqserver (port 8080) works perfectly fine. But, the client shouldn't be expected to book keep who the leader is, should it? Maybe I am misunderstanding some basic concepts.

image

cole-miller commented 11 months ago

But, the client shouldn't be expected to book keep who the leader is, should it? Maybe I am misunderstanding some basic concepts.

Yes, currently this is the client's responsibility. go-dqlite has logic to forward requests to the current leader, and so does the WIP high-level C client library. It's definitely a bit inconvenient if you're writing your own client.

halexh commented 11 months ago

But, the client shouldn't be expected to book keep who the leader is, should it? Maybe I am misunderstanding some basic concepts.

Yes, currently this is the client's responsibility. go-dqlite has logic to forward requests to the current leader, and so does the WIP high-level C client library. It's definitely a bit inconvenient if you're writing your own client.

Speaking of that work in progress C client library. Any update as to when that will be completed?

freeekanayaka commented 11 months ago

But, the client shouldn't be expected to book keep who the leader is, should it? Maybe I am misunderstanding some basic concepts.

Yes, currently this is the client's responsibility. go-dqlite has logic to forward requests to the current leader, and so does the WIP high-level C client library. It's definitely a bit inconvenient if you're writing your own client.

Note that any client of a distributed system must have knowledge of at least some of the nodes that are part of the system. That is necessary because any of those nodes might be down and in that case the client will need to try with another node. That logic might look like:

for (i = 0; i < n; i++) {
    node = all_nodes_the_client_knows_about[i];
    rv = try_connect(node);
    if (rv == 0) {
        break;
   }
}

Since that type of logic must be in place in the client no matter what, it should only be necessary to modify it slightly, like this:

for (i = 0; i < n; i++) {
    node = all_nodes_the_client_knows_about[i];
    rv = try_connect(node);
    if (rv == 0 && node->is_leader) {
        break;
   }
}

All that being said, implementing request forwarding so that the client can connect to any node is surely possible. Note though that it will increase latency (there will be an extra network hop, like a proxy), so all in all perhaps it's better to have the client do a bit of extra work.

cole-miller commented 11 months ago

Speaking of that work in progress C client library. Any update as to when that will be completed?

I apologize for the delay -- I put #525 aside in favor of other work that we saw as higher-priority. I intend to return to it soon.

halexh commented 11 months ago

But, the client shouldn't be expected to book keep who the leader is, should it? Maybe I am misunderstanding some basic concepts.

Yes, currently this is the client's responsibility. go-dqlite has logic to forward requests to the current leader, and so does the WIP high-level C client library. It's definitely a bit inconvenient if you're writing your own client.

Note that any client of a distributed system must have knowledge of at least some of the nodes that are part of the system. That is necessary because any of those nodes might be down and in that case the client will need to try with another node. That logic might look like:

for (i = 0; i < n; i++) {
    node = all_nodes_the_client_knows_about[i];
    rv = try_connect(node);
    if (rv == 0) {
        break;
   }
}

Since that type of logic must be in place in the client no matter what, it should only be necessary to modify it slightly, like this:

for (i = 0; i < n; i++) {
    node = all_nodes_the_client_knows_about[i];
    rv = try_connect(node);
    if (rv == 0 && node->is_leader) {
        break;
   }
}

All that being said, implementing request forwarding so that the client can connect to any node is surely possible. Note though that it will increase latency (there will be an extra network hop, like a proxy), so all in all perhaps it's better to have the client do a bit of extra work.

So I tried doing something similar to this, but what I am seeing is that (going back to my diagram above) instance B's copy of the database does not match instance A's once instance A has closed and handed leadership to instance B.

To give some more detail:

Instance A initializes it's dqlite_server with:

  const char *server_address = "127.0.0.1:8080";
  int err;

  err = dqlite_server_create(aDir, aServer);
  err = dqlite_server_set_address(*aServer, server_address);
  err = dqlite_server_set_auto_bootstrap(*aServer, true);
  err = dqlite_server_start(*aServer);

and Instance B initializes its dqlite_server with:

  const char *addrs[] = {"127.0.0.1:8080"};
  const char *server_address = "127.0.0.1:8082";
  int err;
  int thisClientID = 1234;

  err = dqlite_server_create(aDir, aServer);
  err = dqlite_server_set_address(*aServer, server_address);
  err = dqlite_server_set_auto_join(*aServer, addrs, 1);
  err =
      dqlite_server_set_connect_func(*aServer, endpointConnect, &thisClientID);
  err = dqlite_server_start(*aServer);

I excluded endpointConnect() but it definitely connects to instance A.

From this, it appears that information applied to the dqlite_server leader is not being propagated to other dqlite_servers.

What am I doing incorrectly?

Edit: Got it working. Found a test case that outlines how this should be done, and followed that.