On 2015.10.14 at 13:41:51 +0300, Victor Wagner wrote: > Attached patch which implements client library failover and > loadbalancing as was described in the proposal > <20150818041850.ga5...@wagner.pp.ru>.
New version of patch 1. Handles replication connections correctly (i.e doesn't attempt to check readwrite mode if replication option is on) 2. Some minor improvement recommended by Korry Douglas in <562a9259.4060...@enterprisedb.com> -- Victor Wagner <vi...@wagner.pp.ru>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 0ee018e..ec12fce 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -792,7 +792,7 @@ host=localhost port=5432 dbname=mydb connect_timeout=10 <para> The general form for a connection <acronym>URI</acronym> is: <synopsis> -postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] +postgresql://[user[:password]@][netloc][:port][,netloc[:port]...][/dbname][?param1=value1&...] </synopsis> </para> @@ -809,6 +809,7 @@ postgresql://localhost/mydb postgresql://user@localhost postgresql://user:secret@localhost postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp +postgresql://node1,node2:5433,node3:4432,node4/mydb?hostorder=random&readonly=1 </programlisting> Components of the hierarchical part of the <acronym>URI</acronym> can also be given as parameters. For example: @@ -831,7 +832,9 @@ postgresql:///mydb?host=localhost&port=5433 <para> For improved compatibility with JDBC connection <acronym>URI</acronym>s, instances of parameter <literal>ssl=true</literal> are translated into - <literal>sslmode=require</literal>. + <literal>sslmode=require</literal> and + <literal>loadBalanceHosts=true</literal> into + <literal>hostorder=random</literal>. </para> <para> @@ -841,6 +844,10 @@ postgresql:///mydb?host=localhost&port=5433 postgresql://[2001:db8::1234]/database </synopsis> </para> + <para> + There can be serveral host specifications, optionally accompanied + with port, separated by comma. + </para> <para> The host component is interpreted as described for the parameter <xref @@ -881,6 +888,20 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname when <productname>PostgreSQL</> was built). On machines without Unix-domain sockets, the default is to connect to <literal>localhost</>. </para> + <para> + There can be more than one <literal>host</literal> parameter in + the connect string. In this case these hosts would be considered + alternate entries into same database and if connect to first one + fails, library would try to connect second etc. This can be used + for high availability cluster or for load balancing. See + <xref linkend="libpq-connect-hostorder"> parameter. + </para> + <para> + Network host name can be accompanied with port number, separated by + colon. If so, this port number is used only when connected to + this host. If there is no port number, port specified in the + <xref linkend="libpq-connect-port"> parameter would be used. + </para> </listitem> </varlistentry> @@ -942,8 +963,44 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </para> </listitem> </varlistentry> - + + <varlistentry id="libpq-connect-hostorder" xreflabel="hostorder"> + <term><literal>hostorder</literal></term> + <listitem> + <para> + Specifies how to choose host from list of alternate hosts, + specified in the <xref linkend="libpq-connect-host"> parameter. + </para> + <para> + If value of this argument is <literal>sequential</literal> (the + default) library connects to the hosts in order they specified, + and tries to connect second one only if connection to the first + fails. + </para> + <para> + If value is <literal>random</literal> host to connect is randomly + picked from the list. It allows to balance load between several + cluster nodes. However, currently PostgreSQL doesn't support + multimaster clusters. So, without use of third-party products, + only read-only connections can take advantage from the + load-balancing. See <xref linkend="libpq-connect-readonly"> + </para> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-readonly" xreflabel="readonly"> + <term><literal>readonly</literal></term> + <listitem> + <para> + If this parameter is 0 (the default), upon successful connection + library checks if host is in recovery state, and if it is so, + tries next host in the connect string. If this parameter is + non-zero, connection to warm standby nodes are considered + successful. + </para> + </listitem> + </varlistentry> <varlistentry id="libpq-connect-port" xreflabel="port"> + <term><literal>port</literal></term> <listitem> <para> @@ -985,7 +1042,6 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </para> </listitem> </varlistentry> - <varlistentry id="libpq-connect-connect-timeout" xreflabel="connect_timeout"> <term><literal>connect_timeout</literal></term> <listitem> @@ -996,7 +1052,27 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </para> </listitem> </varlistentry> - + <varlistentry id="libpq-connect-falover-timeout" xreflabel="failover_timeout"> + <term><literal>falover_timeout</literal></term> + <listitem> + <para> + Maximum time to cycilically retry all the hosts in commect string. + (as decimal integer number of seconds). If not specified, then + hosts are tried just once. + </para> + <para> + If we have replicating cluster, and master node fails, it might + take some time to promote one of standby nodes to the new master. + So clients which notice that connect to the master fails, can + already give up attempt to reestablish a connection when new master + became available. + </para> + <para> + Setting this parameter to reasonable time makes library try to + reconnect all the host in cyclically until new master appears. + </para> + </listitem> + </varlistentry> <varlistentry id="libpq-connect-client-encoding" xreflabel="client_encoding"> <term><literal>client_encoding</literal></term> <listitem> @@ -7161,6 +7237,18 @@ user=admin An example file is provided at <filename>share/pg_service.conf.sample</filename>. </para> + <para> + If more than one <literal>host</literal> option present in the section of service file, it + is interpeted as alternate servers for failover or load-balancing. See + <xref linkend="libpq-connect-host"> option in the connect string. + </para> + <para> + For all other options first value takes precedence over later ones. + </para> + <para> + Options, specified in the connect string along with service option + have precedence over values from the service file. + </para> </sect1> diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index f3030fb..5561dcb 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -299,7 +299,16 @@ static const internalPQconninfoOption PQconninfoOptions[] = { {"replication", NULL, NULL, NULL, "Replication", "D", 5, offsetof(struct pg_conn, replication)}, - + /* Parameters added by failover patch */ + {"hostorder", NULL, "sequential", NULL, + "Host order", "", 10, + offsetof(struct pg_conn, hostorder)}, + {"readonly", NULL, "0", NULL, + "Read only", "", 1, + offsetof(struct pg_conn, read_only)}, + {"failover_timeout", NULL, NULL, NULL, + "Failover Timoeut", "", 10, + offsetof(struct pg_conn, failover_timeout)}, /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -380,6 +389,7 @@ static bool getPgPassFilename(char *pgpassfile); static void dot_pg_pass_warning(PGconn *conn); static void default_threadlock(int acquire); +static int try_next_address(PGconn *conn); /* global variable because fe-auth.c needs to access it */ pgthreadlock_t pg_g_threadlock = default_threadlock; @@ -1384,11 +1394,17 @@ setKeepalivesWin32(PGconn *conn) static int connectDBStart(PGconn *conn) { + struct nodeinfo + { + char *host; + char *port; + }; int portnum; char portstr[MAXPGPATH]; struct addrinfo *addrs = NULL; struct addrinfo hint; - const char *node; + struct nodeinfo *nodes, + *node; int ret; if (!conn) @@ -1430,21 +1446,73 @@ connectDBStart(PGconn *conn) if (conn->pghostaddr != NULL && conn->pghostaddr[0] != '\0') { /* Using pghostaddr avoids a hostname lookup */ - node = conn->pghostaddr; + + nodes = calloc(sizeof(struct nodeinfo), 2); + nodes->host = strdup(conn->pghostaddr); hint.ai_family = AF_UNSPEC; hint.ai_flags = AI_NUMERICHOST; } else if (conn->pghost != NULL && conn->pghost[0] != '\0') { /* Using pghost, so we have to look-up the hostname */ - node = conn->pghost; + char *p = conn->pghost, + *q, + *r; + int nodecount = 0, + nodesallocated = 4; + + nodes = malloc(sizeof(struct nodeinfo) * 4); + while (*p) + { + q = p; + r = NULL; + while (*q != ',' && *q != 0) + { + if (*q == ':') + r = q; + if (*q == ']') + r = NULL; /* if there is IPv6, colons before close + * bracket are part of address */ + q++; + } + if (r) + { + nodes[nodecount].port = malloc(q - r ); + strncpy(nodes[nodecount].port, r + 1 , q - r); + /* FIXME need to check that port is numeric */ + nodes[nodecount].port[q - r - 1] = 0; + } + else + { + r = q; + nodes[nodecount].port = NULL; + } + if ((*p) == '[' && *(r - 1) == ']') + { + /* IPv6 address found. Strip brackets */ + p++; + r--; + } + nodes[nodecount].host = malloc(r - p + 1); + strncpy(nodes[nodecount].host, p, r - p); + nodes[nodecount].host[r - p] = 0; + /* skip a comma */ + if (*q) + q++; + nodecount++; + if (nodecount == nodesallocated) + nodes = realloc(nodes, sizeof(struct nodeinfo) * (nodesallocated += 4)); + p = q; + } + nodes[nodecount].host = NULL; + nodes[nodecount].port = NULL; hint.ai_family = AF_UNSPEC; } else { #ifdef HAVE_UNIX_SOCKETS /* pghostaddr and pghost are NULL, so use Unix domain socket */ - node = NULL; + nodes = calloc(sizeof(struct nodeinfo), 2); hint.ai_family = AF_UNIX; UNIXSOCK_PATH(portstr, portnum, conn->pgunixsocket); if (strlen(portstr) >= UNIXSOCK_PATH_BUFLEN) @@ -1454,33 +1522,73 @@ connectDBStart(PGconn *conn) portstr, (int) (UNIXSOCK_PATH_BUFLEN - 1)); conn->options_valid = false; + free(nodes->port); + free(nodes); goto connect_errReturn; } + nodes->port = strdup(portstr); #else /* Without Unix sockets, default to localhost instead */ - node = DefaultHost; + nodes = calloc(sizeof(struct nodeinfo), 2); + nodes->host = strdup(DefaultHost); hint.ai_family = AF_UNSPEC; #endif /* HAVE_UNIX_SOCKETS */ } /* Use pg_getaddrinfo_all() to resolve the address */ - ret = pg_getaddrinfo_all(node, portstr, &hint, &addrs); - if (ret || !addrs) + /* loop over all the host specs in the node variable */ + for (node = nodes; node->host != NULL || node->port != NULL; node++) { - if (node) - appendPQExpBuffer(&conn->errorMessage, - libpq_gettext("could not translate host name \"%s\" to address: %s\n"), - node, gai_strerror(ret)); + struct addrinfo *this_node_addrs; + + ret = pg_getaddrinfo_all(node->host, (node->port ? node->port : portstr), + &hint, &this_node_addrs); + if (ret || !this_node_addrs) + { + if (node->host) + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not translate host name \"%s\" to address: %s\n"), + node->host, gai_strerror(ret)); + else + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not translate Unix-domain socket path \"%s\" to address: %s\n"), + node->port, gai_strerror(ret)); + if (this_node_addrs) + pg_freeaddrinfo_all(hint.ai_family, this_node_addrs); + + /* + * We shouldn't fail here unless there is no valid addrinfos left + */ + continue; + } + /* add this host addrs to addrs */ + if (!addrs) + { + addrs = this_node_addrs; + } else - appendPQExpBuffer(&conn->errorMessage, - libpq_gettext("could not translate Unix-domain socket path \"%s\" to address: %s\n"), - portstr, gai_strerror(ret)); - if (addrs) - pg_freeaddrinfo_all(hint.ai_family, addrs); + { + struct addrinfo *p; + + for (p = addrs; p->ai_next != NULL; p = p->ai_next); + p->ai_next = this_node_addrs; + } + } + /* Free nodes array */ + for (node = nodes; node->host != NULL || node->port != NULL; node++) + { + if (node->host) + free(node->host); + if (node->port) + free(node->port); + } + free(nodes); + /* Check if we've found at least one usable address */ + if (!addrs) + { conn->options_valid = false; goto connect_errReturn; } - #ifdef USE_SSL /* setup values based on SSL mode */ if (conn->sslmode[0] == 'd') /* "disable" */ @@ -1493,12 +1601,23 @@ connectDBStart(PGconn *conn) * Set up to try to connect, with protocol 3.0 as the first attempt. */ conn->addrlist = addrs; - conn->addr_cur = addrs; + + /* + * We cannot just assign first addrs record to addr_cur, because host + * order may be random. So, use try_next_address + */ + conn->addr_cur = NULL; + try_next_address(conn); conn->addrlist_family = hint.ai_family; conn->pversion = PG_PROTOCOL(3, 0); conn->send_appname = true; conn->status = CONNECTION_NEEDED; - + if (conn->failover_timeout) { + conn->failover_finish_time = time(NULL) + + atoi(conn->failover_timeout); + } else { + conn->failover_finish_time = (time_t)0; /* it is in past, so its ok */ + } /* * The code for processing CONNECTION_NEEDED state is in PQconnectPoll(), * so that it can easily be re-executed if needed again during the @@ -1597,6 +1716,76 @@ connectDBComplete(PGconn *conn) } } +/* ------------- + * try_next_address + * Attempts to set next address from the list of known ones. + * Returns 1 if address is choosen and 0 if there are no more addresses + * to try + * Takes into account hostorder parameter + * ------------ + */ + +static int +try_next_address(PGconn *conn) +{ + if (strcmp(conn->hostorder, "sequential") == 0) + { + if (conn->addr_cur == NULL) + { + conn->addr_cur = conn->addrlist; + return 1; + } + else + { + conn->addr_cur = conn->addr_cur->ai_next; + if (conn->addr_cur == NULL && time(NULL) < conn->failover_finish_time) { + /* If failover timeout is set, retry list of hosts from + * the beginning */ + conn->addr_cur = conn->addrlist; + } + return (conn->addr_cur != NULL); + } + } + else if (strcmp(conn->hostorder, "random") == 0) + { + struct addrinfo *choice = NULL, + *current = conn->addrlist; + int count = 0; + /* Initialize random number generator in case if nobody have + * done it before. Use value from rand along with time in case + * random number have been initialized by application. + * Use address of conn structure to load-balance different + * connections in the same app + */ + srand((unsigned int)((long int)conn ^ (long int) time(NULL) ^ + (long int)rand())); + while (current != NULL) + { + if (current == conn->addr_cur) + { + current = current->ai_next; + } + count++; + if ((rand()&0xffff) < 0x10000 / count) + { + choice = current; + } + current = current->ai_next; + } + if (choice == NULL) + return 0; + conn->addr_cur = choice; + return 1; + } + else if (strcmp(conn->hostorder, "parallel") == 0) + { + /* Not implemented yet */ + return 0; + } + return 0; + +} + /* ---------------- * PQconnectPoll * @@ -1674,7 +1863,8 @@ PQconnectPoll(PGconn *conn) case CONNECTION_SSL_STARTUP: case CONNECTION_NEEDED: break; - + case CONNECTION_CHECK_RW: + break; default: appendPQExpBufferStr(&conn->errorMessage, libpq_gettext( @@ -1712,9 +1902,8 @@ keep_going: /* We will come back to here until there is * ignore socket() failure if we have more addresses * to try */ - if (addr_cur->ai_next != NULL) + if (try_next_address(conn)) { - conn->addr_cur = addr_cur->ai_next; continue; } appendPQExpBuffer(&conn->errorMessage, @@ -1733,7 +1922,7 @@ keep_going: /* We will come back to here until there is if (!connectNoDelay(conn)) { pqDropConnection(conn); - conn->addr_cur = addr_cur->ai_next; + try_next_address(conn); continue; } } @@ -1743,7 +1932,7 @@ keep_going: /* We will come back to here until there is libpq_gettext("could not set socket to nonblocking mode: %s\n"), SOCK_STRERROR(SOCK_ERRNO, sebuf, sizeof(sebuf))); pqDropConnection(conn); - conn->addr_cur = addr_cur->ai_next; + try_next_address(conn); continue; } @@ -1754,7 +1943,7 @@ keep_going: /* We will come back to here until there is libpq_gettext("could not set socket to close-on-exec mode: %s\n"), SOCK_STRERROR(SOCK_ERRNO, sebuf, sizeof(sebuf))); pqDropConnection(conn); - conn->addr_cur = addr_cur->ai_next; + try_next_address(conn); continue; } #endif /* F_SETFD */ @@ -1801,7 +1990,7 @@ keep_going: /* We will come back to here until there is if (err) { pqDropConnection(conn); - conn->addr_cur = addr_cur->ai_next; + try_next_address(conn); continue; } } @@ -1892,7 +2081,7 @@ keep_going: /* We will come back to here until there is /* * Try the next address, if any. */ - conn->addr_cur = addr_cur->ai_next; + try_next_address(conn); } /* loop over addresses */ /* @@ -1938,9 +2127,8 @@ keep_going: /* We will come back to here until there is * If more addresses remain, keep trying, just as in the * case where connect() returned failure immediately. */ - if (conn->addr_cur->ai_next != NULL) + if (try_next_address(conn)) { - conn->addr_cur = conn->addr_cur->ai_next; conn->status = CONNECTION_NEEDED; goto keep_going; } @@ -2608,8 +2796,9 @@ keep_going: /* We will come back to here until there is } /* Otherwise, we are open for business! */ - conn->status = CONNECTION_OK; - return PGRES_POLLING_OK; + conn->status = CONNECTION_CHECK_RO; + goto keep_going; + } case CONNECTION_SETENV: @@ -2639,9 +2828,91 @@ keep_going: /* We will come back to here until there is goto error_return; } - /* We are open for business! */ + /* + * check if connection is readonly if we need readwrite one + */ + conn->status = CONNECTION_CHECK_RO; + goto keep_going; + + case CONNECTION_CHECK_RO: + + /* + * consult connection options and check if RO connection is OK + * RO connection is OK if readonly connection is explicitely + * requested or if replication option is set. + */ + if ((conn->read_only && conn->read_only[0] > '0') + ||(conn->replication && conn->replication[0]) + ) + { + conn->status = CONNECTION_OK; + return PGRES_POLLING_OK; + } + /* Otherwise request result pg_is_in_recovery() */ + /* pretend that status is OK for time of sending query */ conn->status = CONNECTION_OK; - return PGRES_POLLING_OK; + PQsendQuery(conn, "SELECT pg_catalog.pg_is_in_recovery()"); + conn->status = CONNECTION_CHECK_RW; + return PGRES_POLLING_READING; + case CONNECTION_CHECK_RW: + { + char *value; + PGresult *res; + + conn->status = CONNECTION_OK; + if (!PQconsumeInput(conn)) + { + conn->status = CONNECTION_BAD; + return PGRES_POLLING_FAILED; + } + if (PQisBusy(conn)) + { + /* Result is not ready yet */ + conn->status = CONNECTION_CHECK_RW; + return PGRES_POLLING_READING; + } + res = PQgetResult(conn); + /* Call PQgetResult second time to clear connection + * state. Should return NULL, so result is ignored + */ + PQgetResult(conn); + if (!res || PQresultStatus(res) != PGRES_TUPLES_OK || + PQntuples(res)!=1) + { + /* + * Something wrong happened with this host. skip to next + * one + */ + conn->status = CONNECTION_NEEDED; + } else { + value = PQgetvalue(res, 0, 0); <<<<<<< BEGIN MERGE CONFLICT: local copy shown first <<<<<<<<<<<<<<< + if (value[0] == 't') ======= COMMON ANCESTOR content follows ============================ + if (strcmp(value, "true") == 0) ======= MERGED IN content follows ================================== + if (value[0]=='t') >>>>>>> END MERGE CONFLICT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + { + conn->status = CONNECTION_NEEDED; + } + } + if (res) PQclear(res); + if (conn->status != CONNECTION_OK) + { + ConnStatusType save_status = conn->status; + + conn->status = CONNECTION_OK; + closePGconn(conn); + conn->sock = PGINVALID_SOCKET; + if (try_next_address(conn)) + { + conn->status = save_status; + goto keep_going; + } + else + { + conn->status = CONNECTION_BAD; + return PGRES_POLLING_FAILED; + } + + } + return PGRES_POLLING_OK; + } default: appendPQExpBuffer(&conn->errorMessage, @@ -3963,6 +4234,8 @@ parseServiceFile(const char *serviceFile, { int linenr = 0, i; + bool hostflag = false; /* true if we already have seen 'host' + * parameter in the service file, and */ FILE *f; char buf[MAXBUFSIZE], *line; @@ -4082,7 +4355,29 @@ parseServiceFile(const char *serviceFile, if (strcmp(options[i].keyword, key) == 0) { if (options[i].val == NULL) + { options[i].val = strdup(val); + /* Set flag that we get value of host option + from this service file, so subsequent + host lines should be appended to it, + not ignored + */ + if (strcmp(key,"host")==0) + hostflag=true; + } + else if (strcmp(key,"host") == 0 && hostflag) + { + /* Old host value is from same service file, + so append new one to it */ + char *old=options[i].val; + int oldlen=strlen(old); + options[i].val=malloc(oldlen+1+strlen(val)+1); + strncpy(options[i].val,old,oldlen); + options[i].val[oldlen]=','; + strcpy(options[i].val+oldlen+1,val); + free(old); + } + if (!options[i].val) { printfPQExpBuffer(errorMessage, @@ -4798,87 +5093,118 @@ conninfo_uri_parse_options(PQconninfoOption *options, const char *uri, */ p = start; } - - /* - * "p" has been incremented past optional URI credential information at - * this point and now points at the "netloc" part of the URI. - * - * Look for IPv6 address. - */ - if (*p == '[') - { - host = ++p; - while (*p && *p != ']') - ++p; - if (!*p) + host = p; + if (*p==':') { + int portnum; + char *portstr; + *(p++)='\0'; + portstr = p; + portnum = 0; + while (*p >= '0' && *p <= '9') { - printfPQExpBuffer(errorMessage, - libpq_gettext("end of string reached when looking for matching \"]\" in IPv6 host address in URI: \"%s\"\n"), - uri); - goto cleanup; + portnum = portnum * 10 + (*(p++) - '0'); } - if (p == host) + if (portnum > 65535 || portnum <1) { printfPQExpBuffer(errorMessage, - libpq_gettext("IPv6 host address may not be empty in URI: \"%s\"\n"), - uri); + libpq_gettext("invalid port number: \"%d\"\n"), + portnum); goto cleanup; } + prevchar = *p; + *p='\0'; + if (*portstr && + !conninfo_storeval(options, "port", portstr, + errorMessage, false, true)); - /* Cut off the bracket and advance */ - *(p++) = '\0'; - - /* - * The address may be followed by a port specifier or a slash or a - * query. - */ - if (*p && *p != ':' && *p != '/' && *p != '?') + } else { + do { - printfPQExpBuffer(errorMessage, - libpq_gettext("unexpected character \"%c\" at position %d in URI (expected \":\" or \"/\"): \"%s\"\n"), - *p, (int) (p - buf + 1), uri); - goto cleanup; - } - } - else - { - /* not an IPv6 address: DNS-named or IPv4 netloc */ - host = p; + if (*p == ',') + p++; - /* - * Look for port specifier (colon) or end of host specifier (slash), - * or query (question mark). - */ - while (*p && *p != ':' && *p != '/' && *p != '?') - ++p; - } - - /* Save the hostname terminator before we null it */ - prevchar = *p; - *p = '\0'; + /* + * "p" has been incremented past optional URI credential information + * at this point and now points at the "netloc" part of the URI. + * + * Look for IPv6 address. + */ + if (*p == '[') + { + char *ipv6start = ++p; - if (*host && - !conninfo_storeval(options, "host", host, - errorMessage, false, true)) - goto cleanup; + while (*p && *p != ']') + ++p; + if (!*p) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("end of string reached when looking for matching \"]\" in IPv6 host address in URI: \"%s\"\n"), + uri); + goto cleanup; + } + if (p == ipv6start) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("IPv6 host address may not be empty in URI: \"%s\"\n"), + uri); + goto cleanup; + } + p++; + /* + * The address may be followed by a port specifier, a comma or a + * slash or a query. + */ + if (*p && *p != ',' && *p != ':' && *p != '/' && *p != '?') + { + printfPQExpBuffer(errorMessage, + libpq_gettext("unexpected character \"%c\" at position %d in URI (expected \":\" or \"/\"): \"%s\"\n"), + *p, (int) (p - buf + 1), uri); + goto cleanup; + } - if (prevchar == ':') - { - const char *port = ++p; /* advance past host terminator */ + } + else + { + /* not an IPv6 address: DNS-named or IPv4 netloc */ - while (*p && *p != '/' && *p != '?') - ++p; + /* + * Look for port specifier (colon) or end of host specifier + * (slash), or query (question mark). + */ + while (*p && *p != ',' && *p != ':' && *p != '/' && *p != '?') + ++p; + } + /* Skip port specifier */ + if (*p == ':') + { + int portnum; + p++; + portnum = 0; + while (*p >= '0' && *p <= '9') + { + portnum = portnum * 10 + (*(p++) - '0'); + } + if (portnum > 65535 || portnum <1) + { + printfPQExpBuffer(errorMessage, + libpq_gettext("invalid port number: \"%d\"\n"), + portnum); + goto cleanup; + } + } + } while (*p == ','); + /* Save the hostname terminator before we null it */ prevchar = *p; *p = '\0'; - - if (*port && - !conninfo_storeval(options, "port", port, - errorMessage, false, true)) + + if (*host && + !conninfo_storeval(options, "host", host, + errorMessage, false, true)) goto cleanup; - } + } if (prevchar && prevchar != '?') { const char *dbname = ++p; /* advance past host terminator */ @@ -5008,6 +5334,16 @@ conninfo_uri_parse_params(char *params, keyword = "sslmode"; value = "require"; } + if ((strcmp(keyword, "loadBalanceHosts") == 0 || + strcmp(keyword, "load_balance_hosts") == 0) && + strcmp(value, "true") == 0) + { + free(keyword); + free(value); + malloced = false; + keyword = "hostorder"; + value = "random"; + } /* * Store the value if the corresponding option exists; ignore @@ -5222,7 +5558,22 @@ conninfo_storeval(PQconninfoOption *connOptions, } if (option->val) + { + if (strcmp(option->keyword, "host") == 0) + { + /* Accumulate multiple hosts in the single string */ + int val_len = strlen(option->val), + new_len = strlen(value); + + free(value_copy); + value_copy = malloc(val_len + 1 + new_len + 1); + strncpy(value_copy, option->val, val_len + 1); + value_copy[val_len] = ','; + strncpy(value_copy + val_len + 1, value, new_len + 1); + } free(option->val); + + } option->val = value_copy; return option; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 828c533..20899bc 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -62,7 +62,11 @@ typedef enum * backend startup. */ CONNECTION_SETENV, /* Negotiating environment. */ CONNECTION_SSL_STARTUP, /* Negotiating SSL. */ - CONNECTION_NEEDED /* Internal state: connect() needed */ + CONNECTION_NEEDED, /* Internal state: connect() needed */ + CONNECTION_CHECK_RO, /* Internal state: need to check is RO + * connection acceptable */ + CONNECTION_CHECK_RW, /* Internal state: waiting that server replies + * if it is in recovery */ } ConnStatusType; typedef enum diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 04d056e..164c858 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -334,7 +334,11 @@ struct pg_conn #if defined(ENABLE_GSS) || defined(ENABLE_SSPI) char *krbsrvname; /* Kerberos service name */ #endif - + char *hostorder; /* How to handle multiple hosts */ + char *read_only; /* If true, we could work with readonly + * standby server */ + char *failover_timeout; /* If no usable server found, how long + * to wait before retry */ /* Optional file to write trace info to */ FILE *Pfdebug; @@ -382,10 +386,12 @@ struct pg_conn struct addrinfo *addrlist; /* list of possible backend addresses */ struct addrinfo *addr_cur; /* the one currently being tried */ int addrlist_family; /* needed to know how to free addrlist */ + time_t failover_finish_time; /* how long to retry host list + *waiting for new master to appear */ PGSetenvStatusType setenv_state; /* for 2.0 protocol only */ const PQEnvironmentOption *next_eo; bool send_appname; /* okay to send application_name? */ - + /* Miscellaneous stuff */ int be_pid; /* PID of backend --- needed for cancels */ int be_key; /* key of backend --- needed for cancels */ diff --git a/src/interfaces/libpq/test/expected.out b/src/interfaces/libpq/test/expected.out index d375e82..4832bdd 100644 --- a/src/interfaces/libpq/test/expected.out +++ b/src/interfaces/libpq/test/expected.out @@ -1,20 +1,20 @@ trying postgresql://uri-user:secret@host:12345/db -user='uri-user' password='secret' dbname='db' host='host' port='12345' (inet) +user='uri-user' password='secret' dbname='db' host='host:12345' (inet) trying postgresql://uri-user@host:12345/db -user='uri-user' dbname='db' host='host' port='12345' (inet) +user='uri-user' dbname='db' host='host:12345' (inet) trying postgresql://uri-user@host/db user='uri-user' dbname='db' host='host' (inet) trying postgresql://host:12345/db -dbname='db' host='host' port='12345' (inet) +dbname='db' host='host:12345' (inet) trying postgresql://host/db dbname='db' host='host' (inet) trying postgresql://uri-user@host:12345/ -user='uri-user' host='host' port='12345' (inet) +user='uri-user' host='host:12345' (inet) trying postgresql://uri-user@host/ user='uri-user' host='host' (inet) @@ -23,10 +23,10 @@ trying postgresql://uri-user@ user='uri-user' (local) trying postgresql://host:12345/ -host='host' port='12345' (inet) +host='host:12345' (inet) trying postgresql://host:12345 -host='host' port='12345' (inet) +host='host:12345' (inet) trying postgresql://host/db dbname='db' host='host' (inet) @@ -62,7 +62,7 @@ trying postgresql://host/db?u%7aer=someotheruser&port=12345 uri-regress: invalid URI query parameter: "uzer" trying postgresql://host:12345?user=uri-user -user='uri-user' host='host' port='12345' (inet) +user='uri-user' host='host:12345' (inet) trying postgresql://host?user=uri-user user='uri-user' host='host' (inet) @@ -71,19 +71,19 @@ trying postgresql://host? host='host' (inet) trying postgresql://[::1]:12345/db -dbname='db' host='::1' port='12345' (inet) +dbname='db' host='[::1]:12345' (inet) trying postgresql://[::1]/db -dbname='db' host='::1' (inet) +dbname='db' host='[::1]' (inet) trying postgresql://[2001:db8::1234]/ -host='2001:db8::1234' (inet) +host='[2001:db8::1234]' (inet) trying postgresql://[200z:db8::1234]/ -host='200z:db8::1234' (inet) +host='[200z:db8::1234]' (inet) trying postgresql://[::1] -host='::1' (inet) +host='[::1]' (inet) trying postgres:// (local) @@ -143,7 +143,7 @@ trying postgres://@host host='host' (inet) trying postgres://host:/ -host='host' (inet) +uri-regress: invalid port number: "0" trying postgres://:12345/ port='12345' (local)
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers