Skip to content

Commit a4081ca

Browse files
committed
review(#10470): a refused catalog query keeps its text and logs no SEVERE in production mode, harden the IT teardown, pin the non-veto edge failure, reference #10473 at the relhasindex flag
1 parent 99aa93f commit a4081ca

4 files changed

Lines changed: 86 additions & 19 deletions

File tree

‎grpc-client/src/test/java/com/arcadedb/remote/grpc/Issue10373RemoteNewEdgeVetoIT.java‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.junit.jupiter.params.provider.EnumSource;
3535

3636
import java.util.List;
37+
import java.util.NoSuchElementException;
3738

3839
import static org.assertj.core.api.Assertions.assertThat;
3940
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -72,15 +73,20 @@ public void setTestConfiguration() {
7273
@Override
7374
public void endTest() {
7475
// HERE AND NOT IN A SEPARATE @AfterEach: THE ORDER OF TWO OF THEM IN ONE CLASS IS UNDEFINED, AND super.endTest() DROPS THE DATABASE
75-
getServerDatabase(0, getDatabaseName()).getSchema().getType(EDGE).getEvents().unregisterListener(createVeto);
76-
if (http != null)
77-
http.close();
78-
if (grpc != null)
79-
grpc.close();
80-
if (grpcServer != null)
81-
grpcServer.close();
82-
GlobalConfiguration.SERVER_PLUGINS.setValue("");
83-
super.endTest();
76+
// A SETUP THAT FAILED BEFORE THE EDGE TYPE OR THE LISTENER EXISTED MUST NOT KEEP THE SERVER UP AND THE PLUGIN SETTING SET
77+
try {
78+
if (createVeto != null && getServerDatabase(0, getDatabaseName()).getSchema().existsType(EDGE))
79+
getServerDatabase(0, getDatabaseName()).getSchema().getType(EDGE).getEvents().unregisterListener(createVeto);
80+
if (http != null)
81+
http.close();
82+
if (grpc != null)
83+
grpc.close();
84+
if (grpcServer != null)
85+
grpcServer.close();
86+
} finally {
87+
GlobalConfiguration.SERVER_PLUGINS.setValue("");
88+
super.endTest();
89+
}
8490
}
8591

8692
@BeforeEach
@@ -113,6 +119,22 @@ void aVetoedNewEdgeIsRaisedAsAVetoNamingTheEdgeAndBothEndpoints(final Driver dri
113119
assertThat(db.lookupByRID(fromRid, true).asVertex().countEdges(Vertex.DIRECTION.OUT, EDGE)).isZero();
114120
}
115121

122+
/**
123+
* An empty CREATE EDGE answer is read as a veto because every other refusal reaches the client as a server error: pinned here, so
124+
* a statement that fails for another reason (an edge type that does not exist) is never reported as a listener's veto.
125+
*/
126+
@ParameterizedTest
127+
@EnumSource(Driver.class)
128+
void aRefusalThatIsNotAVetoIsNotReadAsOne(final Driver driver) {
129+
final RemoteDatabase db = db(driver);
130+
final RID fromRid = newVertex(db);
131+
final RID toRid = newVertex(db);
132+
final Vertex from = db.lookupByRID(fromRid, true).asVertex();
133+
134+
assertThatThrownBy(() -> from.newEdge("NoSuchEdgeType10373", toRid)).isNotInstanceOf(RecordVetoedException.class)
135+
.isNotInstanceOf(NoSuchElementException.class);
136+
}
137+
116138
/** The remote message is the one the embedded engine builds, so a caller reads the same refusal on every path. */
117139
@ParameterizedTest
118140
@EnumSource(Driver.class)

‎postgresw/src/main/java/com/arcadedb/postgres/PostgresCatalog.java‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,25 @@ public class PostgresCatalog {
150150
* that backtracks, sub-selects correlated deep enough to multiply. The client is told so, with PostgreSQL's
151151
* {@code program_limit_exceeded}, rather than handed an empty answer that would look like an empty catalog.
152152
*/
153-
public static final class WorkLimitExceeded extends RuntimeException {
153+
public static final class WorkLimitExceeded extends Refusal {
154154
public static final String SQLSTATE = "54000";
155155

156156
WorkLimitExceeded() {
157-
super("The catalog query needs more work than one statement may spend: simplify it");
157+
super("The catalog query needs more work than one statement may spend: simplify it", SQLSTATE);
158+
}
159+
}
160+
161+
/**
162+
* A catalog query this server refuses, with the SQLSTATE the client acts on. The text is written by this class and holds no
163+
* stored value (at most the name of a relation the client typed), and the cause is the client's query, never a fault of the
164+
* server: so production mode keeps the message, as it does for a refused SET, and logs no SEVERE stack trace for it.
165+
*/
166+
public abstract static class Refusal extends RuntimeException {
167+
public final String sqlState;
168+
169+
Refusal(final String message, final String sqlState) {
170+
super(message);
171+
this.sqlState = sqlState;
158172
}
159173
}
160174

@@ -171,11 +185,11 @@ public static final class WorkLimitExceeded extends RuntimeException {
171185
* with {@link WorkLimitExceeded} does not depend on why the statement was refused. PostgreSQL's own {@code undefined_table}
172186
* would claim the relation does not exist, which a real PostgreSQL has.
173187
*/
174-
public static final class UnsupportedCatalogQuery extends RuntimeException {
188+
public static final class UnsupportedCatalogQuery extends Refusal {
175189
public static final String SQLSTATE = "0A000";
176190

177191
UnsupportedCatalogQuery(final String message) {
178-
super(message);
192+
super(message, SQLSTATE);
179193
}
180194
}
181195

@@ -1222,6 +1236,7 @@ private static Row tableRow(final Context context, final DocumentType type) {
12221236
// relhasindex and hasindexes are FALSE because pg_index is not modelled (issue #10371): every query that would enumerate a
12231237
// table's indexes names it and is refused, so a catalog that said "this relation has indexes" would send psql's \d, which
12241238
// reads the flag first, straight into that refusal. The flag follows what this catalog can show, not what the type has.
1239+
// TODO #10473: model pg_index, then answer !type.getAllIndexes(false).isEmpty() again (here and in pg_tables.hasindexes)
12251240
return singletonRow(context)//
12261241
.with("pg_class", "oid", oid, "relname", type.getName(), "relnamespace", SCHEMA_OID, "relowner", OWNER_OID,//
12271242
"relkind", "r", "relpersistence", "p", "relnatts", type.getPropertyNames().size(),//

‎postgresw/src/main/java/com/arcadedb/postgres/PostgresNetworkExecutor.java‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4191,10 +4191,8 @@ static String sqlStateFor(final Throwable error) {
41914191
return copy.sqlState;
41924192
if (error instanceof PostgresSessionSettings.SettingException setting)
41934193
return setting.sqlState;
4194-
if (error instanceof PostgresCatalog.WorkLimitExceeded)
4195-
return PostgresCatalog.WorkLimitExceeded.SQLSTATE; // program_limit_exceeded (issue #10228)
4196-
if (error instanceof PostgresCatalog.UnsupportedCatalogQuery)
4197-
return PostgresCatalog.UnsupportedCatalogQuery.SQLSTATE; // feature_not_supported (issue #10371)
4194+
if (error instanceof PostgresCatalog.Refusal refusal)
4195+
return refusal.sqlState; // program_limit_exceeded (issue #10228), feature_not_supported (issue #10371)
41984196
return switch (ErrorCategory.of(error)) {
41994197
// serialization_failure - the code drivers auto-retry on. Also a follower's refusal naming the leader (issue
42004198
// #9845): the wire has no redirect code, so the leader is named in the message instead (see withLeaderRedirect)
@@ -4273,8 +4271,10 @@ static String withLeaderRedirect(final String text, final Throwable error) {
42734271
/** {@link #clientMessage} without the leader redirect: the message, or the placeholder in production mode. */
42744272
private String concealableMessage(final String message, final Throwable cause) {
42754273
// A refused SET/DISCARD is text the protocol layer wrote, with no user data in it, and the explanation is what the
4276-
// client is owed; it is also a client mistake, never a server fault (issue #9327)
4277-
if (!server.isProductionMode() || cause instanceof PostgresSessionSettings.SettingException)
4274+
// client is owed; it is also a client mistake, never a server fault (issue #9327). A refused catalog query is the same: text
4275+
// this server wrote, with at most the relation the client named in it (issue #10371)
4276+
if (!server.isProductionMode() || cause instanceof PostgresSessionSettings.SettingException
4277+
|| cause instanceof PostgresCatalog.Refusal)
42784278
return message;
42794279
// The client no longer sees the text, so the server log is the only copy the operator has. A routine, client-driven
42804280
// failure (syntax error, duplicated key, ...) is not an incident: the message alone, no stack trace. Anything the

‎postgresw/src/test/java/com/arcadedb/postgres/Issue8931ProductionErrorConcealmentIT.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,36 @@ void developmentModeKeepsTheFullMessage() throws Exception {
120120
assertThat(error.get('M')).contains(SECRET);
121121
}
122122

123+
/**
124+
* A refused catalog query is text the protocol layer wrote, with no stored value in it (the relation named is the one the client
125+
* typed), and a client mistake rather than a server fault: production mode keeps its explanation, as it does for a refused SET
126+
* (issue #10371). Concealed, the client would read "an internal error happened" for a question it can only fix by asking another.
127+
*/
128+
@Test
129+
void aRefusedCatalogQueryKeepsItsExplanationInProductionMode() throws Exception {
130+
final Map<Character, String> error = withMode("production", (out, in) -> {
131+
sendSimpleQuery(out, "select count(*) from pg_catalog.pg_proc");
132+
return failureOf(readUntilReadyForQuery(in));
133+
});
134+
135+
assertThat(error.get('C')).isEqualTo("0A000");
136+
assertThat(error.get('M')).contains("pg_proc").isNotEqualTo(ArcadeDBServer.CONCEALED_ERROR_MESSAGE);
137+
}
138+
139+
/** The same for the refusal of a catalog query that costs too much (issue #10228): fixed text, a client-driven failure. */
140+
@Test
141+
void aCatalogQueryOverItsWorkBudgetKeepsItsExplanationInProductionMode() throws Exception {
142+
final Map<Character, String> error = withMode("production", (out, in) -> {
143+
sendSimpleQuery(out, "CREATE DOCUMENT TYPE " + "x".repeat(40) + " IF NOT EXISTS");
144+
readUntilReadyForQuery(in);
145+
sendSimpleQuery(out, "select relname from pg_catalog.pg_class where relname ~ '^(.*)(.*)(.*)(.*)(.*)Y$'");
146+
return failureOf(readUntilReadyForQuery(in));
147+
});
148+
149+
assertThat(error.get('C')).isEqualTo("54000");
150+
assertThat(error.get('M')).contains("more work").isNotEqualTo(ArcadeDBServer.CONCEALED_ERROR_MESSAGE);
151+
}
152+
123153
private interface WireScript {
124154
Map<Character, String> run(DataOutputStream out, DataInputStream in) throws Exception;
125155
}

0 commit comments

Comments
 (0)