Skip to content

Commit 8444ed6

Browse files
authored
feat: targetServerType=preferPrimary connection parameter (#2483)
* preferPrimary: documentation, code/logic, tests * preferPrimary: fixed code style * preferPrimary: new tests are in alphabetical order * preferPrimary: simplify MultiHostChooser.candidateIterator(), fix the optimization in there
1 parent 6049852 commit 8444ed6

File tree

6 files changed

+91
-21
lines changed

6 files changed

+91
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ In addition to the standard connection parameters the driver supports a number o
128128
| disableColumnSanitiser | Boolean | false | Enable optimization that disables column name sanitiser |
129129
| assumeMinServerVersion | String | null | Assume the server is at least that version |
130130
| currentSchema | String | null | Specify the schema (or several schema separated by commas) to be set in the search-path |
131-
| targetServerType | String | any | Specifies what kind of server to connect, possible values: any, master, slave (deprecated), secondary, preferSlave (deprecated), preferSecondary |
131+
| targetServerType | String | any | Specifies what kind of server to connect, possible values: any, master, slave (deprecated), secondary, preferSlave (deprecated), preferSecondary, preferPrimary |
132132
| hostRecheckSeconds | Integer | 10 | Specifies period (seconds) after which the host status is checked again in case it has changed |
133133
| loadBalanceHosts | Boolean | false | If disabled hosts are connected in the given order. If enabled hosts are chosen randomly from the set of suitable candidates |
134134
| socketFactory | String | null | Specify a socket factory for socket creation |

docs/documentation/head/connect.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,12 @@ Connection conn = DriverManager.getConnection(url);
475475
* **targetServerType** = String
476476

477477
Allows opening connections to only servers with required state,
478-
the allowed values are any, primary, master, slave, secondary, preferSlave and preferSecondary.
478+
the allowed values are any, primary, master, slave, secondary, preferSlave, preferSecondary and preferPrimary.
479479
The primary/secondary distinction is currently done by observing if the server allows writes.
480480
The value preferSecondary tries to connect to secondary if any are available,
481481
otherwise allows falls back to connecting also to primary.
482+
The value preferPrimary tries to connect to primary if it is available,
483+
otherwise allows falls back to connecting to secondaries available.
482484
- *N.B.* the words master and slave are being deprecated. We will silently accept them, but primary
483485
and secondary are encouraged.
484486

pgjdbc/src/main/java/org/postgresql/PGProperty.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ public enum PGProperty {
693693
"any",
694694
"Specifies what kind of server to connect",
695695
false,
696-
new String [] {"any", "primary", "master", "slave", "secondary", "preferSlave", "preferSecondary"}),
696+
new String [] {"any", "primary", "master", "slave", "secondary", "preferSlave", "preferSecondary", "preferPrimary"}),
697697

698698
/**
699699
* Enable or disable TCP keep-alive. The default is {@code false}.

pgjdbc/src/main/java/org/postgresql/hostchooser/HostRequirement.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public boolean allowConnectingTo(@Nullable HostStatus status) {
4040
public boolean allowConnectingTo(@Nullable HostStatus status) {
4141
return status != HostStatus.ConnectFail;
4242
}
43+
},
44+
preferPrimary {
45+
public boolean allowConnectingTo(@Nullable HostStatus status) {
46+
return status != HostStatus.ConnectFail;
47+
}
4348
};
4449

4550
public abstract boolean allowConnectingTo(@Nullable HostStatus status);

pgjdbc/src/main/java/org/postgresql/hostchooser/MultiHostChooser.java

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import java.util.AbstractList;
1515
import java.util.ArrayList;
1616
import java.util.Arrays;
17-
import java.util.Collections;
1817
import java.util.Iterator;
1918
import java.util.List;
2019
import java.util.Properties;
@@ -47,41 +46,42 @@ public Iterator<CandidateHost> iterator() {
4746
// In case all the candidate hosts are unavailable or do not match, try all the hosts just in case
4847
List<HostSpec> allHosts = Arrays.asList(hostSpecs);
4948
if (loadBalance) {
50-
allHosts = new ArrayList<HostSpec>(allHosts);
51-
Collections.shuffle(allHosts);
49+
allHosts = new ArrayList<>(allHosts);
50+
shuffle(allHosts);
5251
}
5352
res = withReqStatus(targetServerType, allHosts).iterator();
5453
}
5554
return res;
5655
}
5756

5857
private Iterator<CandidateHost> candidateIterator() {
59-
if (targetServerType != HostRequirement.preferSecondary) {
58+
if ( targetServerType != HostRequirement.preferSecondary
59+
&& targetServerType != HostRequirement.preferPrimary ) {
6060
return getCandidateHosts(targetServerType).iterator();
6161
}
6262

63+
HostRequirement preferredServerType =
64+
targetServerType == HostRequirement.preferSecondary
65+
? HostRequirement.secondary
66+
: HostRequirement.primary;
67+
6368
// preferSecondary tries to find secondary hosts first
6469
// Note: sort does not work here since there are "unknown" hosts,
6570
// and that "unknown" might turn out to be master, so we should discard that
6671
// if other secondaries exist
67-
List<CandidateHost> secondaries = getCandidateHosts(HostRequirement.secondary);
72+
// Same logic as the above works for preferPrimary if we replace "secondary"
73+
// with "primary" and vice versa
74+
List<CandidateHost> preferred = getCandidateHosts(preferredServerType);
6875
List<CandidateHost> any = getCandidateHosts(HostRequirement.any);
6976

70-
if (secondaries.isEmpty()) {
71-
return any.iterator();
72-
}
73-
74-
if (any.isEmpty()) {
75-
return secondaries.iterator();
76-
}
77-
78-
if (secondaries.get(secondaries.size() - 1).equals(any.get(0))) {
79-
// When the last secondary's hostspec is the same as the first in "any" list, there's no need
80-
// to attempt to connect it as "secondary"
77+
if ( !preferred.isEmpty() && !any.isEmpty()
78+
&& preferred.get(preferred.size() - 1).hostSpec.equals(any.get(0).hostSpec)) {
79+
// When the last preferred host's hostspec is the same as the first in "any" list, there's no need
80+
// to attempt to connect it as "preferred"
8181
// Note: this is only an optimization
82-
secondaries = rtrim(1, secondaries);
82+
preferred = rtrim(1, preferred);
8383
}
84-
return append(secondaries, any).iterator();
84+
return append(preferred, any).iterator();
8585
}
8686

8787
private List<CandidateHost> getCandidateHosts(HostRequirement hostRequirement) {

pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostsConnectionTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import static org.junit.Assert.fail;
1515
import static org.junit.Assume.assumeTrue;
1616
import static org.postgresql.hostchooser.HostRequirement.any;
17+
import static org.postgresql.hostchooser.HostRequirement.preferPrimary;
1718
import static org.postgresql.hostchooser.HostRequirement.preferSecondary;
1819
import static org.postgresql.hostchooser.HostRequirement.primary;
1920
import static org.postgresql.hostchooser.HostRequirement.secondary;
@@ -237,6 +238,27 @@ public void testConnectToMaster() throws SQLException {
237238
assertGlobalState(secondary1, "Secondary"); // was unknown, so tried to connect in order
238239
}
239240

241+
@Test
242+
public void testConnectToPrimaryFirst() throws SQLException {
243+
getConnection(preferPrimary, true, fake1, primary1, secondary1);
244+
assertRemote(primaryIp);
245+
assertGlobalState(fake1, "ConnectFail");
246+
assertGlobalState(primary1, "Primary");
247+
assertGlobalState(secondary1, null);
248+
249+
getConnection(primary, false, fake1, secondary1, primary1);
250+
assertRemote(primaryIp);
251+
assertGlobalState(fake1, "ConnectFail");
252+
assertGlobalState(primary1, "Primary");
253+
assertGlobalState(secondary1, "Secondary"); // tried as it was unknown
254+
255+
getConnection(preferPrimary, true, fake1, secondary1, primary1);
256+
assertRemote(primaryIp);
257+
assertGlobalState(fake1, "ConnectFail");
258+
assertGlobalState(primary1, "Primary");
259+
assertGlobalState(secondary1, "Secondary");
260+
}
261+
240262
@Test
241263
public void testConnectToPrimaryWithReadonlyTransactionMode() throws SQLException {
242264
con = TestUtil.openPrivilegedDB();
@@ -322,6 +344,47 @@ public void testLoadBalancing() throws SQLException {
322344
assertTrue("Never tried to connect to fake node", fake1FoundTried);
323345
}
324346

347+
@Test
348+
public void testLoadBalancing_preferPrimary() throws SQLException {
349+
Set<String> connectedHosts = new HashSet<String>();
350+
Set<HostSpec> tryConnectedHosts = new HashSet<HostSpec>();
351+
for (int i = 0; i < 20; ++i) {
352+
getConnection(preferPrimary, true, true, fake1, secondary1, secondary2, primary1);
353+
connectedHosts.add(getRemoteHostSpec());
354+
tryConnectedHosts.addAll(hostStatusMap.keySet());
355+
if (tryConnectedHosts.size() == 4) {
356+
break;
357+
}
358+
}
359+
360+
assertRemote(primaryIp);
361+
assertEquals("Connected to hosts other than primary", new HashSet<String>(asList(primaryIp)),
362+
connectedHosts);
363+
assertEquals("Never tried to connect to fake node", 4, tryConnectedHosts.size());
364+
365+
getConnection(preferPrimary, false, true, fake1, secondary1, primary1);
366+
assertRemote(primaryIp);
367+
368+
// connect to secondaries when there's no primary - with load balancing
369+
connectedHosts.clear();
370+
for (int i = 0; i < 20; ++i) {
371+
getConnection(preferPrimary, false, true, fake1, secondary1, secondary2);
372+
connectedHosts.add(getRemoteHostSpec());
373+
if (connectedHosts.size() == 2) {
374+
break;
375+
}
376+
}
377+
assertEquals("Never connected to all secondary hosts", new HashSet<String>(asList(secondaryIP, secondaryIP2)),
378+
connectedHosts);
379+
380+
// connect to secondary when there's no primary
381+
getConnection(preferPrimary, true, true, fake1, secondary1);
382+
assertRemote(secondaryIP);
383+
384+
getConnection(preferPrimary, false, true, fake1, secondary1);
385+
assertRemote(secondaryIP);
386+
}
387+
325388
@Test
326389
public void testLoadBalancing_preferSecondary() throws SQLException {
327390
Set<String> connectedHosts = new HashSet<String>();

0 commit comments

Comments
 (0)