[PATCH] Pattern based listeners for asynchronous messaging (LISTEN/NOTIFY)
Hi
This patch adds an ability to use patterns in *LISTEN* commands. Patch uses
'*SIMILAR TO*' patterns for matching *NOTIFY* channel names (
https://www.postgresql.org/docs/9.0/static/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP
).
This patch is related to old discussion in
/messages/by-id/52693FC5.7070507@gmail.com. This
discussion contains the reasoning behind the pattern based matching of the
channel names.
Patch allows the following.
The listener is registered with following command, for example:
*LISTEN SIMILAR TO 'test%';*
which would match and receive a message from this example notfication:
*NOTIFY test_2;*
Unlistening the above registered pattern:
*UNLISTEN 'test%';*
More examples can be seen from the added regress and isolation tests.
Note that *UNLISTEN* does not allow pattern based unlistening of the
registered listeners. It merely matches the registered pattern by simple
string comparison.
This patch has no know backward compatibility issues with the existing
*LISTEN*/*UNLISTEN* features. This is because patch extends the existing
syntax by accepting quoted strings which define the patterns as opposed to
the existing SQL literals.
The patch also extends the isolationtester by adding an ability to monitor
registered notify messages. This is used to test the existing features as
well as the new pattern based feature. (Does not affect other existing
tests)
Further considerations for this patch:
- Change pattern type, alternatives would be e.g. *LIKE* or POSIX patterns
- Remove '*SIMILAR TO*' from the command (just use quoted string in
form of *LISTEN
'test_%'*)
- Allow *UNLISTEN SIMILAR TO 'xxx'* which would unregister matching
listeners. To me this feels confusing therefore it is not in the patch.
Some notes about patch:
- Most of the changes in async.c
- Regular expression is compiled and finally stored in top memory context
- RE compilation (+error check) happens before transaction commit
- RE is compiled in current transaction memory context from where it is
copied to top memory context on transaction commit (when everything first
succeeds)
- Compiled REs in current transaction are freed on transaction abort
- Compiled REs that were not applied as listeners (duplicates) are freed on
transaction commit
- REs freed when unlistened
- No obvious performance impacts (e.g. normal channel name listening uses
just strcmp)
Thoughts?
Best regards
Markus
Attachments:
listen-pattern.patchapplication/octet-stream; name=listen-pattern.patchDownload
src/backend/commands/async.c | 291 +++++++++++++++++++++----
src/backend/nodes/copyfuncs.c | 5 +-
src/backend/nodes/equalfuncs.c | 5 +-
src/backend/parser/gram.y | 20 +-
src/backend/tcop/utility.c | 6 +-
src/include/commands/async.h | 4 +-
src/include/nodes/parsenodes.h | 5 +-
src/test/isolation/expected/async-notify-2.out | 118 ++++++++++
src/test/isolation/isolation_schedule | 1 +
src/test/isolation/isolationtester.c | 36 ++-
src/test/isolation/specs/async-notify-2.spec | 31 +++
src/test/regress/expected/async.out | 45 +++-
src/test/regress/sql/async.sql | 26 ++-
13 files changed, 530 insertions(+), 63 deletions(-)
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index bacc08e..51acf44 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -138,7 +138,10 @@
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/timestamp.h"
-
+#include "utils/elog.h"
+#include "regex/regex.h"
+#include "catalog/pg_collation.h"
+#include "miscadmin.h"
/*
* Maximum size of a NOTIFY payload, including terminating NULL. This
@@ -286,9 +289,19 @@ static SlruCtlData AsyncCtlData;
*/
#define QUEUE_MAX_PAGE (SLRU_PAGES_PER_SEGMENT * 0x10000 - 1)
+ /*
+ * Currently listened channel consisting of compiled RE
+ * used for pattern listeners and the pattern send by the user.
+ */
+typedef struct ListenChannel
+{
+ regex_t *compiledRegex;
+ char userPattern[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
+} ListenChannel;
+
/*
* listenChannels identifies the channels we are actually listening to
- * (ie, have committed a LISTEN on). It is a simple list of channel names,
+ * (ie, have committed a LISTEN on). It is a list of ListenChannel,
* allocated in TopMemoryContext.
*/
static List *listenChannels = NIL; /* list of C strings */
@@ -313,7 +326,9 @@ typedef enum
typedef struct
{
ListenActionKind action;
- char channel[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
+ bool actionApplied;
+ regex_t *compiledRegex;
+ char userPattern[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
} ListenAction;
static List *pendingActions = NIL; /* list of ListenAction */
@@ -369,12 +384,12 @@ bool Trace_notify = false;
/* local function prototypes */
static bool asyncQueuePagePrecedes(int p, int q);
-static void queue_listen(ListenActionKind action, const char *channel);
+static void queue_listen(ListenActionKind action, const char *pattern, bool isSimilarToPattern);
static void Async_UnlistenOnExit(int code, Datum arg);
static void Exec_ListenPreCommit(void);
-static void Exec_ListenCommit(const char *channel);
-static void Exec_UnlistenCommit(const char *channel);
-static void Exec_UnlistenAllCommit(void);
+static bool Exec_ListenCommit(const char *pattern, regex_t *compiledRegex);
+static bool Exec_UnlistenCommit(const char *pattern);
+static bool Exec_UnlistenAllCommit(void);
static bool IsListeningOn(const char *channel);
static void asyncQueueUnregister(void);
static bool asyncQueueIsFull(void);
@@ -594,6 +609,36 @@ Async_Notify(const char *channel, const char *payload)
}
/*
+* compile_regex
+* Compiles RE pattern into a compiled RE.
+*
+* Returns result code from pg_regcomp.
+*/
+static int
+compile_regex(const char *pattern, regex_t *compiled_regex)
+{
+ pg_wchar *wcharpattern;
+ int resregcomp;
+ int lenwchar;
+ int lenpattern = strlen(pattern);
+
+ wcharpattern = (pg_wchar *)palloc((lenpattern + 1) * sizeof(pg_wchar));
+ lenwchar = pg_mb2wchar_with_len(pattern,
+ wcharpattern,
+ lenpattern);
+
+ resregcomp = pg_regcomp(compiled_regex,
+ wcharpattern,
+ lenwchar,
+ REG_ADVANCED,
+ DEFAULT_COLLATION_OID);
+
+ pfree(wcharpattern);
+
+ return resregcomp;
+}
+
+/*
* queue_listen
* Common code for listen, unlisten, unlisten all commands.
*
@@ -602,10 +647,15 @@ Async_Notify(const char *channel, const char *payload)
* commit.
*/
static void
-queue_listen(ListenActionKind action, const char *channel)
+queue_listen(ListenActionKind action, const char *pattern, bool isSimilarToPattern)
{
MemoryContext oldcontext;
ListenAction *actrec;
+ regex_t compreg;
+ regex_t *pcompreg;
+ int resregcomp;
+ char errormsg[100];
+ Datum datum;
/*
* Unlike Async_Notify, we don't try to collapse out duplicates. It would
@@ -615,11 +665,44 @@ queue_listen(ListenActionKind action, const char *channel)
*/
oldcontext = MemoryContextSwitchTo(CurTransactionContext);
+ if (isSimilarToPattern)
+ {
+ /* convert to regex pattern */
+ datum = DirectFunctionCall1(similar_escape, CStringGetTextDatum(pattern));
+
+ /*
+ * Regex pattern is now compiled to ensure any errors are captured at this point.
+ * Compiled regex is copied to top memory context when we reach transaction commit.
+ * If compiled RE was not applied as a listener then it is freed at transaction commit.
+ */
+ resregcomp = compile_regex(TextDatumGetCString(datum), &compreg);
+
+ if (resregcomp != REG_OKAY)
+ {
+ MemoryContextSwitchTo(oldcontext);
+
+ CHECK_FOR_INTERRUPTS();
+ pg_regerror(resregcomp, &compreg, errormsg, sizeof(errormsg));
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("invalid regular expression: %s", errormsg)));
+ }
+
+ pcompreg = palloc(sizeof(regex_t));
+ memcpy(pcompreg, &compreg, sizeof(regex_t));
+ }
+ else
+ {
+ pcompreg = NULL;
+ }
+
/* space for terminating null is included in sizeof(ListenAction) */
- actrec = (ListenAction *) palloc(offsetof(ListenAction, channel) +
- strlen(channel) + 1);
+ actrec = (ListenAction *) palloc(offsetof(ListenAction, userPattern) +
+ strlen(pattern) + 1);
actrec->action = action;
- strcpy(actrec->channel, channel);
+ actrec->actionApplied = false;
+ actrec->compiledRegex = pcompreg;
+ strcpy(actrec->userPattern, pattern);
pendingActions = lappend(pendingActions, actrec);
@@ -632,12 +715,12 @@ queue_listen(ListenActionKind action, const char *channel)
* This is executed by the SQL listen command.
*/
void
-Async_Listen(const char *channel)
+Async_Listen(const char *pattern, bool isSimilarToPattern)
{
if (Trace_notify)
- elog(DEBUG1, "Async_Listen(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Async_Listen(%s,%d)", pattern, MyProcPid);
- queue_listen(LISTEN_LISTEN, channel);
+ queue_listen(LISTEN_LISTEN, pattern, isSimilarToPattern);
}
/*
@@ -646,16 +729,16 @@ Async_Listen(const char *channel)
* This is executed by the SQL unlisten command.
*/
void
-Async_Unlisten(const char *channel)
+Async_Unlisten(const char *pattern)
{
if (Trace_notify)
- elog(DEBUG1, "Async_Unlisten(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Async_Unlisten(%s,%d)", pattern, MyProcPid);
/* If we couldn't possibly be listening, no need to queue anything */
if (pendingActions == NIL && !unlistenExitRegistered)
return;
- queue_listen(LISTEN_UNLISTEN, channel);
+ queue_listen(LISTEN_UNLISTEN, pattern, false);
}
/*
@@ -673,7 +756,7 @@ Async_UnlistenAll(void)
if (pendingActions == NIL && !unlistenExitRegistered)
return;
- queue_listen(LISTEN_UNLISTEN_ALL, "");
+ queue_listen(LISTEN_UNLISTEN_ALL, "", false);
}
/*
@@ -714,10 +797,10 @@ pg_listening_channels(PG_FUNCTION_ARGS)
while (*lcp != NULL)
{
- char *channel = (char *) lfirst(*lcp);
+ ListenChannel *channel = (ListenChannel *) lfirst(*lcp);
*lcp = lnext(*lcp);
- SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(channel));
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(channel->userPattern));
}
SRF_RETURN_DONE(funcctx);
@@ -890,13 +973,13 @@ AtCommit_Notify(void)
switch (actrec->action)
{
case LISTEN_LISTEN:
- Exec_ListenCommit(actrec->channel);
+ actrec->actionApplied = Exec_ListenCommit(actrec->userPattern, actrec->compiledRegex);
break;
case LISTEN_UNLISTEN:
- Exec_UnlistenCommit(actrec->channel);
+ actrec->actionApplied = Exec_UnlistenCommit(actrec->userPattern);
break;
case LISTEN_UNLISTEN_ALL:
- Exec_UnlistenAllCommit();
+ actrec->actionApplied = Exec_UnlistenAllCommit();
break;
}
}
@@ -1000,14 +1083,23 @@ Exec_ListenPreCommit(void)
*
* Add the channel to the list of channels we are listening on.
*/
-static void
-Exec_ListenCommit(const char *channel)
+static bool
+Exec_ListenCommit(const char *pattern, regex_t *compiledRegex)
{
+ ListCell *p;
MemoryContext oldcontext;
+ ListenChannel *lchan;
+ regex_t *copiedcompreg;
- /* Do nothing if we are already listening on this channel */
- if (IsListeningOn(channel))
- return;
+ /* Do nothing if we are already using this pattern for listening */
+
+ foreach(p, listenChannels)
+ {
+ ListenChannel *lchan = (ListenChannel *)lfirst(p);
+
+ if (strcmp(lchan->userPattern, pattern) == 0)
+ return false;
+ }
/*
* Add the new channel name to listenChannels.
@@ -1017,9 +1109,31 @@ Exec_ListenCommit(const char *channel)
* doesn't seem worth trying to guard against that, but maybe improve this
* later.
*/
+
oldcontext = MemoryContextSwitchTo(TopMemoryContext);
- listenChannels = lappend(listenChannels, pstrdup(channel));
+
+ if (compiledRegex != NULL)
+ {
+ /* copy the compiled RE to top memory context */
+
+ copiedcompreg = (regex_t *)palloc(sizeof(regex_t));
+ memcpy(copiedcompreg, compiledRegex, sizeof(regex_t));
+ }
+ else
+ {
+ copiedcompreg = NULL;
+ }
+
+ lchan = (ListenChannel *)palloc(offsetof(ListenChannel, userPattern) +
+ strlen(pattern) + 1);
+ lchan->compiledRegex = copiedcompreg;
+ strcpy(lchan->userPattern, pattern);
+
+ listenChannels = lappend(listenChannels, lchan);
+
MemoryContextSwitchTo(oldcontext);
+
+ return true;
}
/*
@@ -1027,24 +1141,35 @@ Exec_ListenCommit(const char *channel)
*
* Remove the specified channel name from listenChannels.
*/
-static void
-Exec_UnlistenCommit(const char *channel)
+static bool
+Exec_UnlistenCommit(const char *pattern)
{
ListCell *q;
ListCell *prev;
+ bool found;
if (Trace_notify)
- elog(DEBUG1, "Exec_UnlistenCommit(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Exec_UnlistenCommit(%s,%d)", pattern, MyProcPid);
+ found = false;
prev = NULL;
foreach(q, listenChannels)
{
- char *lchan = (char *) lfirst(q);
+ ListenChannel *lchan = (ListenChannel *) lfirst(q);
- if (strcmp(lchan, channel) == 0)
+ if (strcmp(lchan->userPattern, pattern) == 0)
{
+ if (lchan->compiledRegex != NULL)
+ {
+ pg_regfree(lchan->compiledRegex);
+ pfree(lchan->compiledRegex);
+ }
+
listenChannels = list_delete_cell(listenChannels, q, prev);
+
pfree(lchan);
+
+ found = true;
break;
}
prev = q;
@@ -1054,6 +1179,8 @@ Exec_UnlistenCommit(const char *channel)
* We do not complain about unlistening something not being listened;
* should we?
*/
+
+ return found;
}
/*
@@ -1061,14 +1188,34 @@ Exec_UnlistenCommit(const char *channel)
*
* Unlisten on all channels for this backend.
*/
-static void
+static bool
Exec_UnlistenAllCommit(void)
{
+ ListCell *p;
+ bool is_empty;
+
if (Trace_notify)
elog(DEBUG1, "Exec_UnlistenAllCommit(%d)", MyProcPid);
+ is_empty = true;
+
+ foreach(p, listenChannels)
+ {
+ ListenChannel *lchan = (ListenChannel *)lfirst(p);
+
+ if (lchan->compiledRegex != NULL)
+ {
+ pg_regfree(lchan->compiledRegex);
+ pfree(lchan->compiledRegex);
+ }
+
+ is_empty = false;
+ }
+
list_free_deep(listenChannels);
listenChannels = NIL;
+
+ return is_empty;
}
/*
@@ -1163,16 +1310,66 @@ ProcessCompletedNotifies(void)
static bool
IsListeningOn(const char *channel)
{
- ListCell *p;
+ ListCell *p;
+ pg_wchar *wcharchannel;
+ int lenwchar;
+ int resregexec;
+ char errormsg[100];
+ bool matches;
+
+ wcharchannel = NULL;
+ matches = false;
foreach(p, listenChannels)
{
- char *lchan = (char *) lfirst(p);
+ ListenChannel *lchan = (ListenChannel *) lfirst(p);
- if (strcmp(lchan, channel) == 0)
- return true;
+ if (lchan->compiledRegex == NULL)
+ {
+ if (strcmp(lchan->userPattern, channel) == 0)
+ {
+ matches = true;
+ break;
+ }
+ }
+ else
+ {
+ if (wcharchannel == NULL)
+ {
+ /* Convert channel string to wide characters */
+ wcharchannel = (pg_wchar *)palloc((strlen(channel) + 1) * sizeof(pg_wchar));
+ lenwchar = pg_mb2wchar_with_len(channel, wcharchannel, strlen(channel));
+ }
+
+ /* Check RE match */
+ resregexec = pg_regexec(lchan->compiledRegex, wcharchannel, lenwchar, 0, NULL, 0, NULL, 0);
+
+ if (resregexec != REG_OKAY && resregexec != REG_NOMATCH)
+ {
+ pfree(wcharchannel);
+ wcharchannel = NULL;
+
+ CHECK_FOR_INTERRUPTS();
+ pg_regerror(resregexec, lchan->compiledRegex, errormsg, sizeof(errormsg));
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("regular expression failed: %s", errormsg)));
+ }
+
+ if (resregexec == REG_OKAY)
+ {
+ matches = true;
+ break;
+ }
+ }
}
- return false;
+
+ if (wcharchannel != NULL)
+ {
+ pfree(wcharchannel);
+ }
+
+ return matches;
}
/*
@@ -2149,6 +2346,20 @@ AsyncExistsPendingNotify(const char *channel, const char *payload)
static void
ClearPendingActionsAndNotifies(void)
{
+ ListCell *p;
+
+ /* free compiled REs that were not added as new listeners */
+ foreach(p, pendingActions)
+ {
+ ListenAction *actrec = (ListenAction *)lfirst(p);
+
+ if (!actrec->actionApplied && actrec->compiledRegex != NULL)
+ {
+ pg_regfree(actrec->compiledRegex);
+ pfree(actrec->compiledRegex);
+ }
+ }
+
/*
* We used to have to explicitly deallocate the list members and nodes,
* because they were malloc'd. Now, since we know they are palloc'd in
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 45a04b0..dd1d1ef 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3545,7 +3545,8 @@ _copyListenStmt(const ListenStmt *from)
{
ListenStmt *newnode = makeNode(ListenStmt);
- COPY_STRING_FIELD(conditionname);
+ COPY_STRING_FIELD(pattern);
+ COPY_SCALAR_FIELD(isSimilarToPattern);
return newnode;
}
@@ -3555,7 +3556,7 @@ _copyUnlistenStmt(const UnlistenStmt *from)
{
UnlistenStmt *newnode = makeNode(UnlistenStmt);
- COPY_STRING_FIELD(conditionname);
+ COPY_STRING_FIELD(pattern);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 8d92c03..a3ac36b 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1482,7 +1482,8 @@ _equalNotifyStmt(const NotifyStmt *a, const NotifyStmt *b)
static bool
_equalListenStmt(const ListenStmt *a, const ListenStmt *b)
{
- COMPARE_STRING_FIELD(conditionname);
+ COMPARE_STRING_FIELD(pattern);
+ COMPARE_SCALAR_FIELD(isSimilarToPattern);
return true;
}
@@ -1490,7 +1491,7 @@ _equalListenStmt(const ListenStmt *a, const ListenStmt *b)
static bool
_equalUnlistenStmt(const UnlistenStmt *a, const UnlistenStmt *b)
{
- COMPARE_STRING_FIELD(conditionname);
+ COMPARE_STRING_FIELD(pattern);
return true;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4b1ce09..b7a4cf4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9451,7 +9451,15 @@ notify_payload:
ListenStmt: LISTEN ColId
{
ListenStmt *n = makeNode(ListenStmt);
- n->conditionname = $2;
+ n->pattern = $2;
+ n->isSimilarToPattern = false;
+ $$ = (Node *)n;
+ }
+ | LISTEN SIMILAR TO Sconst
+ {
+ ListenStmt *n = makeNode(ListenStmt);
+ n->pattern = $4;
+ n->isSimilarToPattern = true;
$$ = (Node *)n;
}
;
@@ -9460,13 +9468,19 @@ UnlistenStmt:
UNLISTEN ColId
{
UnlistenStmt *n = makeNode(UnlistenStmt);
- n->conditionname = $2;
+ n->pattern = $2;
+ $$ = (Node *)n;
+ }
+ | UNLISTEN Sconst
+ {
+ UnlistenStmt *n = makeNode(UnlistenStmt);
+ n->pattern = $2;
$$ = (Node *)n;
}
| UNLISTEN '*'
{
UnlistenStmt *n = makeNode(UnlistenStmt);
- n->conditionname = NULL;
+ n->pattern = NULL;
$$ = (Node *)n;
}
;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ddacac8..9251ad2 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -630,7 +630,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
PreventCommandDuringRecovery("LISTEN");
CheckRestrictedOperation("LISTEN");
- Async_Listen(stmt->conditionname);
+ Async_Listen(stmt->pattern, stmt->isSimilarToPattern);
}
break;
@@ -640,8 +640,8 @@ standard_ProcessUtility(PlannedStmt *pstmt,
PreventCommandDuringRecovery("UNLISTEN");
CheckRestrictedOperation("UNLISTEN");
- if (stmt->conditionname)
- Async_Unlisten(stmt->conditionname);
+ if (stmt->pattern)
+ Async_Unlisten(stmt->pattern);
else
Async_UnlistenAll();
}
diff --git a/src/include/commands/async.h b/src/include/commands/async.h
index 939711d..f86a482 100644
--- a/src/include/commands/async.h
+++ b/src/include/commands/async.h
@@ -34,8 +34,8 @@ extern void NotifyMyFrontEnd(const char *channel,
/* notify-related SQL statements */
extern void Async_Notify(const char *channel, const char *payload);
-extern void Async_Listen(const char *channel);
-extern void Async_Unlisten(const char *channel);
+extern void Async_Listen(const char *pattern, bool isSimilarToPattern);
+extern void Async_Unlisten(const char *pattern);
extern void Async_UnlistenAll(void);
/* perform (or cancel) outbound notify processing at transaction commit */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5f2a4a7..3f9fe9d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2889,7 +2889,8 @@ typedef struct NotifyStmt
typedef struct ListenStmt
{
NodeTag type;
- char *conditionname; /* condition name to listen on */
+ bool isSimilarToPattern;
+ char *pattern; /* condition pattern to listen on */
} ListenStmt;
/* ----------------------
@@ -2899,7 +2900,7 @@ typedef struct ListenStmt
typedef struct UnlistenStmt
{
NodeTag type;
- char *conditionname; /* name to unlisten on, or NULL for all */
+ char *pattern; /* condition pattern to unlisten on, or NULL for all */
} UnlistenStmt;
/* ----------------------
diff --git a/src/test/isolation/expected/async-notify-2.out b/src/test/isolation/expected/async-notify-2.out
new file mode 100644
index 0000000..081cae3
--- /dev/null
+++ b/src/test/isolation/expected/async-notify-2.out
@@ -0,0 +1,118 @@
+Parsed test spec with 2 sessions
+
+starting permutation: listen_normal notify consume
+step listen_normal: LISTEN test;
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+
+starting permutation: listen_pattern_1 notify consume
+step listen_pattern_1: LISTEN SIMILAR TO 'te%';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+ASYNC NOTIFY of 'test_2' with payload '1' received
+ASYNC NOTIFY of 'test_2' with payload '2' received
+ASYNC NOTIFY of 'test_2' with payload '3' received
+ASYNC NOTIFY of 'test_2' with payload '4' received
+ASYNC NOTIFY of 'test_2' with payload '5' received
+
+starting permutation: listen_pattern_2 notify consume
+step listen_pattern_2: LISTEN SIMILAR TO 'test';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+
+starting permutation: listen_pattern_3 notify consume
+step listen_pattern_3: LISTEN SIMILAR TO 'te';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+
+starting permutation: listen_pattern_invalid notify consume
+step listen_pattern_invalid: LISTEN SIMILAR TO '*';
+ERROR: invalid regular expression: quantifier operand invalid
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+
+starting permutation: listen_normal listen_pattern_1 unlisten_1 notify consume
+step listen_normal: LISTEN test;
+step listen_pattern_1: LISTEN SIMILAR TO 'te%';
+step unlisten_1: UNLISTEN 't%';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+ASYNC NOTIFY of 'test_2' with payload '1' received
+ASYNC NOTIFY of 'test_2' with payload '2' received
+ASYNC NOTIFY of 'test_2' with payload '3' received
+ASYNC NOTIFY of 'test_2' with payload '4' received
+ASYNC NOTIFY of 'test_2' with payload '5' received
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 32c965b..a25cdeb 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -60,5 +60,6 @@ test: alter-table-3
test: create-trigger
test: sequence-ddl
test: async-notify
+test: async-notify-2
test: vacuum-reltuples
test: timeouts
diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c
index ba8082c..b1eb17d 100644
--- a/src/test/isolation/isolationtester.c
+++ b/src/test/isolation/isolationtester.c
@@ -46,7 +46,8 @@ static bool try_complete_step(Step *step, int flags);
static int step_qsort_cmp(const void *a, const void *b);
static int step_bsearch_cmp(const void *a, const void *b);
-static void printResultSet(PGresult *res);
+static void printResultSet(PGresult *res, PGconn *conn);
+static void printAsyncNotify(PGconn *conn);
/* close all connections and exit */
static void
@@ -487,7 +488,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[0], testspec->setupsqls[i]);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -505,7 +506,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[i + 1], testspec->sessions[i]->setupsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -640,7 +641,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[i + 1], testspec->sessions[i]->teardownsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -659,7 +660,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[0], testspec->teardownsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[0]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -821,9 +822,10 @@ try_complete_step(Step *step, int flags)
switch (PQresultStatus(res))
{
case PGRES_COMMAND_OK:
+ printAsyncNotify(conn);
break;
case PGRES_TUPLES_OK:
- printResultSet(res);
+ printResultSet(res, conn);
break;
case PGRES_FATAL_ERROR:
if (step->errormsg != NULL)
@@ -860,7 +862,7 @@ try_complete_step(Step *step, int flags)
}
static void
-printResultSet(PGresult *res)
+printResultSet(PGresult *res, PGconn *conn)
{
int nFields;
int i,
@@ -879,4 +881,24 @@ printResultSet(PGresult *res)
printf("%-15s", PQgetvalue(res, i, j));
printf("\n");
}
+
+ printAsyncNotify(conn);
+}
+
+static void
+printAsyncNotify(PGconn *conn)
+{
+ PGnotify *notify;
+
+ while ((notify = PQnotifies(conn)) != NULL)
+ {
+ if (notify->extra[0])
+ printf("ASYNC NOTIFY of '%s' with payload '%s' received\n",
+ notify->relname, notify->extra);
+ else
+ printf("ASYNC NOTIFY of '%s' received\n",
+ notify->relname);
+
+ PQfreemem(notify);
+ }
}
diff --git a/src/test/isolation/specs/async-notify-2.spec b/src/test/isolation/specs/async-notify-2.spec
new file mode 100644
index 0000000..dd1d38f
--- /dev/null
+++ b/src/test/isolation/specs/async-notify-2.spec
@@ -0,0 +1,31 @@
+# Verify that messages are consumed from the notify queue.
+
+session "listener"
+step "listen_normal" { LISTEN test; }
+step "listen_pattern_1" { LISTEN SIMILAR TO 'te%'; }
+step "listen_pattern_2" { LISTEN SIMILAR TO 'test'; }
+step "listen_pattern_3" { LISTEN SIMILAR TO 'te'; }
+step "listen_pattern_invalid" { LISTEN SIMILAR TO '*'; }
+step "unlisten_1" { UNLISTEN 't%'; }
+step "consume" { BEGIN; END; }
+teardown { UNLISTEN *; }
+
+session "notifier"
+step "notify"
+{
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+}
+
+# Should print first notify channel
+permutation "listen_normal" "notify" "consume"
+# Should print both notify channels
+permutation "listen_pattern_1" "notify" "consume"
+# Should print first notify channel
+permutation "listen_pattern_2" "notify" "consume"
+# Should not print either notify channels
+permutation "listen_pattern_3" "notify" "consume"
+# Should fail to invalid RE pattern
+permutation "listen_pattern_invalid" "notify" "consume"
+# Test that UNLISTEN with a pattern does not work as a RE matcher
+permutation "listen_normal" "listen_pattern_1" "unlisten_1" "notify" "consume"
\ No newline at end of file
diff --git a/src/test/regress/expected/async.out b/src/test/regress/expected/async.out
index 19cbe38..963b1d0 100644
--- a/src/test/regress/expected/async.out
+++ b/src/test/regress/expected/async.out
@@ -27,11 +27,54 @@ SELECT pg_notify(NULL,'sample message1');
ERROR: channel name cannot be empty
SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
ERROR: channel name too long
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- src/test/isolation/specs/async-notify-2.spec tests for actual usage.
NOTIFY notify_async2;
LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_%';
UNLISTEN notify_async2;
+UNLISTEN 'notify_%';
+UNLISTEN 'notify_(%';
UNLISTEN *;
+-- Should fail. Invalid LISTEN command
+LISTEN *;
+ERROR: syntax error at or near "*"
+LINE 1: LISTEN *;
+ ^
+LISTEN notify_%;
+ERROR: syntax error at or near "%"
+LINE 1: LISTEN notify_%;
+ ^
+LISTEN SIMILAR TO 'notify_(%';
+ERROR: invalid regular expression: parentheses () not balanced
+LISTEN SIMILAR TO '*';
+ERROR: invalid regular expression: quantifier operand invalid
+-- Should contain two listeners
+LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_async2';
+LISTEN SIMILAR TO 'notify_%';
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+ notify_async2
+ notify_%
+(2 rows)
+
+-- Should contain one listener
+UNLISTEN 'notify_%';
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+ notify_async2
+(1 row)
+
+-- Should not contain listeners
+UNLISTEN *;
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+(0 rows)
+
-- Should return zero while there are no pending notifications.
-- src/test/isolation/specs/async-notify.spec tests for actual usage.
SELECT pg_notification_queue_usage();
diff --git a/src/test/regress/sql/async.sql b/src/test/regress/sql/async.sql
index 40f6e01..69d8f98 100644
--- a/src/test/regress/sql/async.sql
+++ b/src/test/regress/sql/async.sql
@@ -12,12 +12,36 @@ SELECT pg_notify('','sample message1');
SELECT pg_notify(NULL,'sample message1');
SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- src/test/isolation/specs/async-notify-2.spec tests for actual usage.
NOTIFY notify_async2;
LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_%';
UNLISTEN notify_async2;
+UNLISTEN 'notify_%';
+UNLISTEN 'notify_(%';
UNLISTEN *;
+-- Should fail. Invalid LISTEN command
+LISTEN *;
+LISTEN notify_%;
+LISTEN SIMILAR TO 'notify_(%';
+LISTEN SIMILAR TO '*';
+
+-- Should contain two listeners
+LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_async2';
+LISTEN SIMILAR TO 'notify_%';
+SELECT pg_listening_channels();
+
+-- Should contain one listener
+UNLISTEN 'notify_%';
+SELECT pg_listening_channels();
+
+-- Should not contain listeners
+UNLISTEN *;
+SELECT pg_listening_channels();
+
-- Should return zero while there are no pending notifications.
-- src/test/isolation/specs/async-notify.spec tests for actual usage.
SELECT pg_notification_queue_usage();
On 7/31/17 16:13, Markus Sintonen wrote:
This patch has no know backward compatibility issues with the existing
/LISTEN///UNLISTEN/ features. This is because patch extends the existing
syntax by accepting quoted strings which define the patterns as opposed
to the existing SQL literals.
I don't see that in the patch. Your patch changes the syntax LISTEN
ColId to mean a regular expression.
Even then, having LISTEN "foo" and LISTEN 'foo' mean different things
would probably be confusing.
I would think about specifying an operator somewhere in the syntax, like
you have with LISTEN SIMILAR TO. It would even be nice if a
non-built-in operator could be used for matching names.
Documentation is missing in the patch.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 07/31/17 16:30, Peter Eisentraut wrote:
I would think about specifying an operator somewhere in the syntax, like
you have with LISTEN SIMILAR TO. It would even be nice if a
non-built-in operator could be used for matching names.
Hmm ... as I was reading through the email thread, I saw a suggestion
was once made to look at ltree, and then I noticed the patch as presented
had simply gone with regular expressions instead. I wonder if there'd be
a way to work it so an operator can be specified, and allow you to treat
the names as (say) ltree labels if that suited your application.
-Chap
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
The following does not work:
LISTEN 'foo%'
Neither this:
LISTEN SIMILAR TO "foo%"
This works:
LISTEN "foo%"
But it does not act as a pattern.
We could change the SIMILAR TO something like following (accepting also
type of the pattern), for example:
LISTEN PATTERN 'foo%' TYPE 'similar'
LISTEN PATTERN 'foo*' TYPE 'ltree'
Or
LISTEN PATTERN 'foo%' USING 'similar'
Or
LISTEN MATCH 'foo%' USING 'similar'
Or
LISTEN MATCH 'foo%' TYPE 'similar'
Documentation is coming up.
2017-07-31 23:30 GMT+03:00 Peter Eisentraut <
peter.eisentraut@2ndquadrant.com>:
Show quoted text
On 7/31/17 16:13, Markus Sintonen wrote:
This patch has no know backward compatibility issues with the existing
/LISTEN///UNLISTEN/ features. This is because patch extends the existing
syntax by accepting quoted strings which define the patterns as opposed
to the existing SQL literals.I don't see that in the patch. Your patch changes the syntax LISTEN
ColId to mean a regular expression.Even then, having LISTEN "foo" and LISTEN 'foo' mean different things
would probably be confusing.I would think about specifying an operator somewhere in the syntax, like
you have with LISTEN SIMILAR TO. It would even be nice if a
non-built-in operator could be used for matching names.Documentation is missing in the patch.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Aug 1, 2017 at 8:13 AM, Markus Sintonen
<markus.sintonen@gmail.com> wrote:
This patch adds an ability to use patterns in LISTEN commands. Patch uses
'SIMILAR TO' patterns for matching NOTIFY channel names
(https://www.postgresql.org/docs/9.0/static/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP).This patch is related to old discussion in
/messages/by-id/52693FC5.7070507@gmail.com. This
discussion contains the reasoning behind the pattern based matching of the
channel names.
Nice idea.
The "async" regression test consistently crashes on my FreeBSD box
when built with -O2. It doesn't crash on another system I tried, and
I think that's just luck, because this:
+ /* convert to regex pattern */
+ datum = DirectFunctionCall1(similar_escape,
CStringGetTextDatum(pattern));
... is calling a function that takes two arguments, but passing only
one. The second argument is random junk, so similar_escape bombs when
it does this:
esc_text = PG_GETARG_TEXT_PP(1);
(lldb) bt
* thread #1, name = 'postgres', stop reason = signal SIGSEGV
* frame #0: postgres`pg_detoast_datum_packed(datum=0x0000000000000000)
at fmgr.c:1865
frame #1: postgres`similar_escape(fcinfo=0x00007fffffffd080) at regexp.c:686
frame #2: postgres`DirectFunctionCall1Coll(func=(postgres`similar_escape
at regexp.c:659), collation=<unavailable>, arg1=<unavailable>) at
fmgr.c:717
frame #3: postgres`queue_listen(action=LISTEN_LISTEN,
pattern="notify_%", isSimilarToPattern=<unavailable>) at async.c:671
frame #4: postgres`standard_ProcessUtility(pstmt=0x0000000801824df0,
queryString="LISTEN SIMILAR TO 'notify_%';",
context=PROCESS_UTILITY_TOPLEVEL, params=0x0000000000000000,
queryEnv=0x0000000000000000, dest=0x0000000801824ee8,
completionTag=<unavailable>) at utility.c:633
frame #5: postgres`PortalRunUtility(portal=0x0000000801960040,
pstmt=0x0000000801824df0, isTopLevel='\x01',
setHoldSnapshot=<unavailable>, dest=<unavailable>, completionTag="")
at pquery.c:1178
frame #6: postgres`PortalRunMulti(portal=0x0000000801960040,
isTopLevel='\x01', setHoldSnapshot='\0', dest=0x0000000801824ee8,
altdest=0x0000000801824ee8, completionTag="") at pquery.c:0
frame #7: postgres`PortalRun(portal=0x0000000801960040,
count=<unavailable>, isTopLevel='\x01', run_once=<unavailable>,
dest=<unavailable>, altdest=<unavailable>,
completionTag=<unavailable>) at pquery.c:799
frame #8: postgres`exec_simple_query(query_string="LISTEN SIMILAR
TO 'notify_%';") at postgres.c:1099
frame #9: postgres`PostgresMain(argc=<unavailable>,
argv=<unavailable>, dbname=<unavailable>, username=<unavailable>) at
postgres.c:0
frame #10: postgres`PostmasterMain [inlined] BackendRun at postmaster.c:4357
frame #11: postgres`PostmasterMain [inlined] BackendStartup at
postmaster.c:4029
frame #12: postgres`PostmasterMain at postmaster.c:1753
frame #13: postgres`PostmasterMain(argc=<unavailable>,
argv=0x00000008018d11c0) at postmaster.c:1361
frame #14: postgres`main(argc=<unavailable>, argv=<unavailable>)
at main.c:228
frame #15: 0x00000000004823af postgres`_start + 383
--
Thomas Munro
http://www.enterprisedb.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I also encountered this when I built it with different configuration. I
attached updated patch with the correct number of arguments to
'similar_escape'. I also added preliminary documentation to the patch.
(Unfortunately unable to currently compile the documentation for testing
purpose on Windows probably because of commit https://github.com/
postgres/postgres/commit/510074f9f0131a04322d6a3d2a51c87e6db243f9. I
followed https://www.postgresql.org/docs/devel/static/install-
windows-full.html#idm45412738673840.)
What do you think about the syntax? There was a suggestion to specify type
of the pattern (eg ltree extension) but to me this feels like a overkill.
One option here would be eg:
LISTEN PATTERN 'foo%' TYPE 'similar'
LISTEN PATTERN 'foo.*' TYPE 'ltree'
... and so on
BR
-Markus
2017-08-19 2:36 GMT+03:00 Thomas Munro <thomas.munro@enterprisedb.com>:
Show quoted text
On Tue, Aug 1, 2017 at 8:13 AM, Markus Sintonen
<markus.sintonen@gmail.com> wrote:This patch adds an ability to use patterns in LISTEN commands. Patch uses
'SIMILAR TO' patterns for matching NOTIFY channel names
(https://www.postgresql.org/docs/9.0/static/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP).
This patch is related to old discussion in
/messages/by-id/52693FC5.7070507@gmail.com. This
discussion contains the reasoning behind the pattern based matching ofthe
channel names.
Nice idea.
The "async" regression test consistently crashes on my FreeBSD box
when built with -O2. It doesn't crash on another system I tried, and
I think that's just luck, because this:+ /* convert to regex pattern */ + datum = DirectFunctionCall1(similar_escape, CStringGetTextDatum(pattern));... is calling a function that takes two arguments, but passing only
one. The second argument is random junk, so similar_escape bombs when
it does this:esc_text = PG_GETARG_TEXT_PP(1);
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
listen-pattern.patchapplication/octet-stream; name=listen-pattern.patchDownload
diff --git a/doc/src/sgml/ref/listen.sgml b/doc/src/sgml/ref/listen.sgml
index 9cd53b0..b6fae56 100644
--- a/doc/src/sgml/ref/listen.sgml
+++ b/doc/src/sgml/ref/listen.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
LISTEN <replaceable class="PARAMETER">channel</replaceable>
+LISTEN SIMILAR TO '<replaceable class="PARAMETER">pattern</replaceable>'
</synopsis>
</refsynopsisdiv>
@@ -31,9 +32,11 @@ LISTEN <replaceable class="PARAMETER">channel</replaceable>
<para>
<command>LISTEN</command> registers the current session as a
listener on the notification channel named <replaceable
- class="PARAMETER">channel</replaceable>.
+ class="PARAMETER">channel</replaceable>. Session can listen
+ to multiple notification channels simultaneously by using
+ <command>LISTEN SIMILAR TO</command> pattern.
If the current session is already registered as a listener for
- this notification channel, nothing is done.
+ this notification channel or pattern, nothing is done.
</para>
<para>
@@ -84,6 +87,16 @@ LISTEN <replaceable class="PARAMETER">channel</replaceable>
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="PARAMETER">pattern</replaceable></term>
+ <listitem>
+ <para>
+ <link linkend="functions-similarto-regexp"><literal>SIMILAR TO</></link>
+ pattern (any string constant). Used to match notification channel names.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</refsect1>
@@ -113,6 +126,9 @@ LISTEN <replaceable class="PARAMETER">channel</replaceable>
LISTEN virtual;
NOTIFY virtual;
Asynchronous notification "virtual" received from server process with PID 8448.
+LISTEN SIMILAR TO 'foo%';
+NOTIFY foobar;
+Asynchronous notification "foobar" received from server process with PID 8448.
</programlisting></para>
</refsect1>
diff --git a/doc/src/sgml/ref/notify.sgml b/doc/src/sgml/ref/notify.sgml
index 3389aa0..c660754 100644
--- a/doc/src/sgml/ref/notify.sgml
+++ b/doc/src/sgml/ref/notify.sgml
@@ -33,7 +33,10 @@ NOTIFY <replaceable class="PARAMETER">channel</replaceable> [ , <replaceable cla
with an optional <quote>payload</> string to each client application that
has previously executed
<command>LISTEN <replaceable class="parameter">channel</></command>
- for the specified channel name in the current database.
+ for the specified channel name in the current database. Client application
+ can also listen to multiple channel names using pattern matching by
+ <command>LISTEN SIMILAR TO '<replaceable class="parameter">pattern</>'</command>
+ command.
Notifications are visible to all users.
</para>
@@ -210,6 +213,10 @@ Asynchronous notification "virtual" with payload "This is the payload" received
LISTEN foo;
SELECT pg_notify('fo' || 'o', 'pay' || 'load');
Asynchronous notification "foo" with payload "payload" received from server process with PID 14728.
+
+LISTEN SIMILAR TO 'foo%';
+NOTIFY foobar;
+Asynchronous notification "foobar" received from server process with PID 8448.
</programlisting></para>
</refsect1>
diff --git a/doc/src/sgml/ref/unlisten.sgml b/doc/src/sgml/ref/unlisten.sgml
index f7c3c47..46908f9 100644
--- a/doc/src/sgml/ref/unlisten.sgml
+++ b/doc/src/sgml/ref/unlisten.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
-UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | * }
+UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | '<replaceable class="PARAMETER">channel_pattern</replaceable>' | * }
</synopsis>
</refsynopsisdiv>
@@ -34,9 +34,10 @@ UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | * }
<command>UNLISTEN</command> cancels any existing registration of
the current <productname>PostgreSQL</productname> session as a
listener on the notification channel named <replaceable
- class="PARAMETER">channel</replaceable>. The special wildcard
- <literal>*</literal> cancels all listener registrations for the
- current session.
+ class="PARAMETER">channel</replaceable>. Existing pattern based listeners
+ are unregistered using '<replaceable class="PARAMETER">channel_pattern</replaceable>'
+ string constant. The special wildcard <literal>*</literal> cancels all listener
+ registrations for the current session.
</para>
<para>
@@ -59,6 +60,15 @@ UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | * }
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="PARAMETER">channel_pattern</replaceable></term>
+ <listitem>
+ <para>
+ Pattern or channel name to unregister (any string constant).
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>*</literal></term>
@@ -88,6 +98,12 @@ UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | * }
A transaction that has executed <command>UNLISTEN</command> cannot be
prepared for two-phase commit.
</para>
+
+ <para>
+ <command>UNLISTEN</command> '<replaceable class="PARAMETER">channel_pattern</replaceable>'
+ only unregisters a single pattern that is equal to a previously
+ registered pattern. See below for an example.
+ </para>
</refsect1>
<refsect1>
@@ -100,6 +116,9 @@ UNLISTEN { <replaceable class="PARAMETER">channel</replaceable> | * }
LISTEN virtual;
NOTIFY virtual;
Asynchronous notification "virtual" received from server process with PID 8448.
+LISTEN SIMILAR TO 'foo%';
+NOTIFY foobar;
+Asynchronous notification "foobar" received from server process with PID 8448.
</programlisting>
</para>
@@ -109,9 +128,22 @@ Asynchronous notification "virtual" received from server process with PID 8448.
<programlisting>
UNLISTEN virtual;
+UNLISTEN 'foo%';
NOTIFY virtual;
+NOTIFY foobar;
-- no NOTIFY event is received
</programlisting></para>
+
+ <para>
+ <command>UNLISTEN</command> '<replaceable class="PARAMETER">channel_pattern</replaceable>'
+ cannot be used in a pattern matching way:
+
+<programlisting>
+LISTEN SIMILAR TO 'foobar%';
+UNLISTEN 'foo%'; -- this has no effect
+NOTIFY foobarbaz;
+Asynchronous notification "foobarbaz" received from server process with PID 8448.
+</programlisting></para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index bacc08e..16af303 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -138,7 +138,10 @@
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/timestamp.h"
-
+#include "utils/elog.h"
+#include "regex/regex.h"
+#include "catalog/pg_collation.h"
+#include "miscadmin.h"
/*
* Maximum size of a NOTIFY payload, including terminating NULL. This
@@ -286,9 +289,19 @@ static SlruCtlData AsyncCtlData;
*/
#define QUEUE_MAX_PAGE (SLRU_PAGES_PER_SEGMENT * 0x10000 - 1)
+ /*
+ * Currently listened channel consisting of compiled RE
+ * used for pattern listeners and the pattern send by the user.
+ */
+typedef struct ListenChannel
+{
+ regex_t *compiledRegex;
+ char userPattern[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
+} ListenChannel;
+
/*
* listenChannels identifies the channels we are actually listening to
- * (ie, have committed a LISTEN on). It is a simple list of channel names,
+ * (ie, have committed a LISTEN on). It is a list of ListenChannel,
* allocated in TopMemoryContext.
*/
static List *listenChannels = NIL; /* list of C strings */
@@ -313,7 +326,9 @@ typedef enum
typedef struct
{
ListenActionKind action;
- char channel[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
+ bool actionApplied;
+ regex_t *compiledRegex;
+ char userPattern[FLEXIBLE_ARRAY_MEMBER]; /* nul-terminated string */
} ListenAction;
static List *pendingActions = NIL; /* list of ListenAction */
@@ -369,12 +384,12 @@ bool Trace_notify = false;
/* local function prototypes */
static bool asyncQueuePagePrecedes(int p, int q);
-static void queue_listen(ListenActionKind action, const char *channel);
+static void queue_listen(ListenActionKind action, const char *pattern, bool isSimilarToPattern);
static void Async_UnlistenOnExit(int code, Datum arg);
static void Exec_ListenPreCommit(void);
-static void Exec_ListenCommit(const char *channel);
-static void Exec_UnlistenCommit(const char *channel);
-static void Exec_UnlistenAllCommit(void);
+static bool Exec_ListenCommit(const char *pattern, regex_t *compiledRegex);
+static bool Exec_UnlistenCommit(const char *pattern);
+static bool Exec_UnlistenAllCommit(void);
static bool IsListeningOn(const char *channel);
static void asyncQueueUnregister(void);
static bool asyncQueueIsFull(void);
@@ -594,6 +609,36 @@ Async_Notify(const char *channel, const char *payload)
}
/*
+* compile_regex
+* Compiles RE pattern into a compiled RE.
+*
+* Returns result code from pg_regcomp.
+*/
+static int
+compile_regex(const char *pattern, regex_t *compiled_regex)
+{
+ pg_wchar *wcharpattern;
+ int resregcomp;
+ int lenwchar;
+ int lenpattern = strlen(pattern);
+
+ wcharpattern = (pg_wchar *)palloc((lenpattern + 1) * sizeof(pg_wchar));
+ lenwchar = pg_mb2wchar_with_len(pattern,
+ wcharpattern,
+ lenpattern);
+
+ resregcomp = pg_regcomp(compiled_regex,
+ wcharpattern,
+ lenwchar,
+ REG_ADVANCED,
+ DEFAULT_COLLATION_OID);
+
+ pfree(wcharpattern);
+
+ return resregcomp;
+}
+
+/*
* queue_listen
* Common code for listen, unlisten, unlisten all commands.
*
@@ -602,10 +647,15 @@ Async_Notify(const char *channel, const char *payload)
* commit.
*/
static void
-queue_listen(ListenActionKind action, const char *channel)
+queue_listen(ListenActionKind action, const char *pattern, bool isSimilarToPattern)
{
MemoryContext oldcontext;
ListenAction *actrec;
+ regex_t compreg;
+ regex_t *pcompreg;
+ int resregcomp;
+ char errormsg[100];
+ Datum datum;
/*
* Unlike Async_Notify, we don't try to collapse out duplicates. It would
@@ -615,11 +665,46 @@ queue_listen(ListenActionKind action, const char *channel)
*/
oldcontext = MemoryContextSwitchTo(CurTransactionContext);
+ if (isSimilarToPattern)
+ {
+ /* convert to regex pattern */
+ datum = DirectFunctionCall2(similar_escape,
+ CStringGetTextDatum(pattern),
+ CStringGetTextDatum("\\"));
+
+ /*
+ * Regex pattern is now compiled to ensure any errors are captured at this point.
+ * Compiled regex is copied to top memory context when we reach transaction commit.
+ * If compiled RE was not applied as a listener then it is freed at transaction commit.
+ */
+ resregcomp = compile_regex(TextDatumGetCString(datum), &compreg);
+
+ if (resregcomp != REG_OKAY)
+ {
+ MemoryContextSwitchTo(oldcontext);
+
+ CHECK_FOR_INTERRUPTS();
+ pg_regerror(resregcomp, &compreg, errormsg, sizeof(errormsg));
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("invalid regular expression: %s", errormsg)));
+ }
+
+ pcompreg = palloc(sizeof(regex_t));
+ memcpy(pcompreg, &compreg, sizeof(regex_t));
+ }
+ else
+ {
+ pcompreg = NULL;
+ }
+
/* space for terminating null is included in sizeof(ListenAction) */
- actrec = (ListenAction *) palloc(offsetof(ListenAction, channel) +
- strlen(channel) + 1);
+ actrec = (ListenAction *) palloc(offsetof(ListenAction, userPattern) +
+ strlen(pattern) + 1);
actrec->action = action;
- strcpy(actrec->channel, channel);
+ actrec->actionApplied = false;
+ actrec->compiledRegex = pcompreg;
+ strcpy(actrec->userPattern, pattern);
pendingActions = lappend(pendingActions, actrec);
@@ -632,12 +717,12 @@ queue_listen(ListenActionKind action, const char *channel)
* This is executed by the SQL listen command.
*/
void
-Async_Listen(const char *channel)
+Async_Listen(const char *pattern, bool isSimilarToPattern)
{
if (Trace_notify)
- elog(DEBUG1, "Async_Listen(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Async_Listen(%s,%d)", pattern, MyProcPid);
- queue_listen(LISTEN_LISTEN, channel);
+ queue_listen(LISTEN_LISTEN, pattern, isSimilarToPattern);
}
/*
@@ -646,16 +731,16 @@ Async_Listen(const char *channel)
* This is executed by the SQL unlisten command.
*/
void
-Async_Unlisten(const char *channel)
+Async_Unlisten(const char *pattern)
{
if (Trace_notify)
- elog(DEBUG1, "Async_Unlisten(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Async_Unlisten(%s,%d)", pattern, MyProcPid);
/* If we couldn't possibly be listening, no need to queue anything */
if (pendingActions == NIL && !unlistenExitRegistered)
return;
- queue_listen(LISTEN_UNLISTEN, channel);
+ queue_listen(LISTEN_UNLISTEN, pattern, false);
}
/*
@@ -673,7 +758,7 @@ Async_UnlistenAll(void)
if (pendingActions == NIL && !unlistenExitRegistered)
return;
- queue_listen(LISTEN_UNLISTEN_ALL, "");
+ queue_listen(LISTEN_UNLISTEN_ALL, "", false);
}
/*
@@ -714,10 +799,10 @@ pg_listening_channels(PG_FUNCTION_ARGS)
while (*lcp != NULL)
{
- char *channel = (char *) lfirst(*lcp);
+ ListenChannel *channel = (ListenChannel *) lfirst(*lcp);
*lcp = lnext(*lcp);
- SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(channel));
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(channel->userPattern));
}
SRF_RETURN_DONE(funcctx);
@@ -890,13 +975,13 @@ AtCommit_Notify(void)
switch (actrec->action)
{
case LISTEN_LISTEN:
- Exec_ListenCommit(actrec->channel);
+ actrec->actionApplied = Exec_ListenCommit(actrec->userPattern, actrec->compiledRegex);
break;
case LISTEN_UNLISTEN:
- Exec_UnlistenCommit(actrec->channel);
+ actrec->actionApplied = Exec_UnlistenCommit(actrec->userPattern);
break;
case LISTEN_UNLISTEN_ALL:
- Exec_UnlistenAllCommit();
+ actrec->actionApplied = Exec_UnlistenAllCommit();
break;
}
}
@@ -1000,14 +1085,23 @@ Exec_ListenPreCommit(void)
*
* Add the channel to the list of channels we are listening on.
*/
-static void
-Exec_ListenCommit(const char *channel)
+static bool
+Exec_ListenCommit(const char *pattern, regex_t *compiledRegex)
{
+ ListCell *p;
MemoryContext oldcontext;
+ ListenChannel *lchan;
+ regex_t *copiedcompreg;
- /* Do nothing if we are already listening on this channel */
- if (IsListeningOn(channel))
- return;
+ /* Do nothing if we are already using this pattern for listening */
+
+ foreach(p, listenChannels)
+ {
+ ListenChannel *lchan = (ListenChannel *)lfirst(p);
+
+ if (strcmp(lchan->userPattern, pattern) == 0)
+ return false;
+ }
/*
* Add the new channel name to listenChannels.
@@ -1017,9 +1111,31 @@ Exec_ListenCommit(const char *channel)
* doesn't seem worth trying to guard against that, but maybe improve this
* later.
*/
+
oldcontext = MemoryContextSwitchTo(TopMemoryContext);
- listenChannels = lappend(listenChannels, pstrdup(channel));
+
+ if (compiledRegex != NULL)
+ {
+ /* copy the compiled RE to top memory context */
+
+ copiedcompreg = (regex_t *)palloc(sizeof(regex_t));
+ memcpy(copiedcompreg, compiledRegex, sizeof(regex_t));
+ }
+ else
+ {
+ copiedcompreg = NULL;
+ }
+
+ lchan = (ListenChannel *)palloc(offsetof(ListenChannel, userPattern) +
+ strlen(pattern) + 1);
+ lchan->compiledRegex = copiedcompreg;
+ strcpy(lchan->userPattern, pattern);
+
+ listenChannels = lappend(listenChannels, lchan);
+
MemoryContextSwitchTo(oldcontext);
+
+ return true;
}
/*
@@ -1027,24 +1143,35 @@ Exec_ListenCommit(const char *channel)
*
* Remove the specified channel name from listenChannels.
*/
-static void
-Exec_UnlistenCommit(const char *channel)
+static bool
+Exec_UnlistenCommit(const char *pattern)
{
ListCell *q;
ListCell *prev;
+ bool found;
if (Trace_notify)
- elog(DEBUG1, "Exec_UnlistenCommit(%s,%d)", channel, MyProcPid);
+ elog(DEBUG1, "Exec_UnlistenCommit(%s,%d)", pattern, MyProcPid);
+ found = false;
prev = NULL;
foreach(q, listenChannels)
{
- char *lchan = (char *) lfirst(q);
+ ListenChannel *lchan = (ListenChannel *) lfirst(q);
- if (strcmp(lchan, channel) == 0)
+ if (strcmp(lchan->userPattern, pattern) == 0)
{
+ if (lchan->compiledRegex != NULL)
+ {
+ pg_regfree(lchan->compiledRegex);
+ pfree(lchan->compiledRegex);
+ }
+
listenChannels = list_delete_cell(listenChannels, q, prev);
+
pfree(lchan);
+
+ found = true;
break;
}
prev = q;
@@ -1054,6 +1181,8 @@ Exec_UnlistenCommit(const char *channel)
* We do not complain about unlistening something not being listened;
* should we?
*/
+
+ return found;
}
/*
@@ -1061,14 +1190,34 @@ Exec_UnlistenCommit(const char *channel)
*
* Unlisten on all channels for this backend.
*/
-static void
+static bool
Exec_UnlistenAllCommit(void)
{
+ ListCell *p;
+ bool is_empty;
+
if (Trace_notify)
elog(DEBUG1, "Exec_UnlistenAllCommit(%d)", MyProcPid);
+ is_empty = true;
+
+ foreach(p, listenChannels)
+ {
+ ListenChannel *lchan = (ListenChannel *)lfirst(p);
+
+ if (lchan->compiledRegex != NULL)
+ {
+ pg_regfree(lchan->compiledRegex);
+ pfree(lchan->compiledRegex);
+ }
+
+ is_empty = false;
+ }
+
list_free_deep(listenChannels);
listenChannels = NIL;
+
+ return is_empty;
}
/*
@@ -1163,16 +1312,66 @@ ProcessCompletedNotifies(void)
static bool
IsListeningOn(const char *channel)
{
- ListCell *p;
+ ListCell *p;
+ pg_wchar *wcharchannel;
+ int lenwchar;
+ int resregexec;
+ char errormsg[100];
+ bool matches;
+
+ wcharchannel = NULL;
+ matches = false;
foreach(p, listenChannels)
{
- char *lchan = (char *) lfirst(p);
+ ListenChannel *lchan = (ListenChannel *) lfirst(p);
- if (strcmp(lchan, channel) == 0)
- return true;
+ if (lchan->compiledRegex == NULL)
+ {
+ if (strcmp(lchan->userPattern, channel) == 0)
+ {
+ matches = true;
+ break;
+ }
+ }
+ else
+ {
+ if (wcharchannel == NULL)
+ {
+ /* Convert channel string to wide characters */
+ wcharchannel = (pg_wchar *)palloc((strlen(channel) + 1) * sizeof(pg_wchar));
+ lenwchar = pg_mb2wchar_with_len(channel, wcharchannel, strlen(channel));
+ }
+
+ /* Check RE match */
+ resregexec = pg_regexec(lchan->compiledRegex, wcharchannel, lenwchar, 0, NULL, 0, NULL, 0);
+
+ if (resregexec != REG_OKAY && resregexec != REG_NOMATCH)
+ {
+ pfree(wcharchannel);
+ wcharchannel = NULL;
+
+ CHECK_FOR_INTERRUPTS();
+ pg_regerror(resregexec, lchan->compiledRegex, errormsg, sizeof(errormsg));
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("regular expression failed: %s", errormsg)));
+ }
+
+ if (resregexec == REG_OKAY)
+ {
+ matches = true;
+ break;
+ }
+ }
}
- return false;
+
+ if (wcharchannel != NULL)
+ {
+ pfree(wcharchannel);
+ }
+
+ return matches;
}
/*
@@ -2149,6 +2348,20 @@ AsyncExistsPendingNotify(const char *channel, const char *payload)
static void
ClearPendingActionsAndNotifies(void)
{
+ ListCell *p;
+
+ /* free compiled REs that were not added as new listeners */
+ foreach(p, pendingActions)
+ {
+ ListenAction *actrec = (ListenAction *)lfirst(p);
+
+ if (!actrec->actionApplied && actrec->compiledRegex != NULL)
+ {
+ pg_regfree(actrec->compiledRegex);
+ pfree(actrec->compiledRegex);
+ }
+ }
+
/*
* We used to have to explicitly deallocate the list members and nodes,
* because they were malloc'd. Now, since we know they are palloc'd in
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 7204169..ebc2e16 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3547,7 +3547,8 @@ _copyListenStmt(const ListenStmt *from)
{
ListenStmt *newnode = makeNode(ListenStmt);
- COPY_STRING_FIELD(conditionname);
+ COPY_STRING_FIELD(pattern);
+ COPY_SCALAR_FIELD(isSimilarToPattern);
return newnode;
}
@@ -3557,7 +3558,7 @@ _copyUnlistenStmt(const UnlistenStmt *from)
{
UnlistenStmt *newnode = makeNode(UnlistenStmt);
- COPY_STRING_FIELD(conditionname);
+ COPY_STRING_FIELD(pattern);
return newnode;
}
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 8d92c03..a3ac36b 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1482,7 +1482,8 @@ _equalNotifyStmt(const NotifyStmt *a, const NotifyStmt *b)
static bool
_equalListenStmt(const ListenStmt *a, const ListenStmt *b)
{
- COMPARE_STRING_FIELD(conditionname);
+ COMPARE_STRING_FIELD(pattern);
+ COMPARE_SCALAR_FIELD(isSimilarToPattern);
return true;
}
@@ -1490,7 +1491,7 @@ _equalListenStmt(const ListenStmt *a, const ListenStmt *b)
static bool
_equalUnlistenStmt(const UnlistenStmt *a, const UnlistenStmt *b)
{
- COMPARE_STRING_FIELD(conditionname);
+ COMPARE_STRING_FIELD(pattern);
return true;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d0de99..3bb1605 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9420,7 +9420,15 @@ notify_payload:
ListenStmt: LISTEN ColId
{
ListenStmt *n = makeNode(ListenStmt);
- n->conditionname = $2;
+ n->pattern = $2;
+ n->isSimilarToPattern = false;
+ $$ = (Node *)n;
+ }
+ | LISTEN SIMILAR TO Sconst
+ {
+ ListenStmt *n = makeNode(ListenStmt);
+ n->pattern = $4;
+ n->isSimilarToPattern = true;
$$ = (Node *)n;
}
;
@@ -9429,13 +9437,19 @@ UnlistenStmt:
UNLISTEN ColId
{
UnlistenStmt *n = makeNode(UnlistenStmt);
- n->conditionname = $2;
+ n->pattern = $2;
+ $$ = (Node *)n;
+ }
+ | UNLISTEN Sconst
+ {
+ UnlistenStmt *n = makeNode(UnlistenStmt);
+ n->pattern = $2;
$$ = (Node *)n;
}
| UNLISTEN '*'
{
UnlistenStmt *n = makeNode(UnlistenStmt);
- n->conditionname = NULL;
+ n->pattern = NULL;
$$ = (Node *)n;
}
;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 775477c..d7b4fb6 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -630,7 +630,7 @@ standard_ProcessUtility(PlannedStmt *pstmt,
PreventCommandDuringRecovery("LISTEN");
CheckRestrictedOperation("LISTEN");
- Async_Listen(stmt->conditionname);
+ Async_Listen(stmt->pattern, stmt->isSimilarToPattern);
}
break;
@@ -640,8 +640,8 @@ standard_ProcessUtility(PlannedStmt *pstmt,
PreventCommandDuringRecovery("UNLISTEN");
CheckRestrictedOperation("UNLISTEN");
- if (stmt->conditionname)
- Async_Unlisten(stmt->conditionname);
+ if (stmt->pattern)
+ Async_Unlisten(stmt->pattern);
else
Async_UnlistenAll();
}
diff --git a/src/include/commands/async.h b/src/include/commands/async.h
index 939711d..f86a482 100644
--- a/src/include/commands/async.h
+++ b/src/include/commands/async.h
@@ -34,8 +34,8 @@ extern void NotifyMyFrontEnd(const char *channel,
/* notify-related SQL statements */
extern void Async_Notify(const char *channel, const char *payload);
-extern void Async_Listen(const char *channel);
-extern void Async_Unlisten(const char *channel);
+extern void Async_Listen(const char *pattern, bool isSimilarToPattern);
+extern void Async_Unlisten(const char *pattern);
extern void Async_UnlistenAll(void);
/* perform (or cancel) outbound notify processing at transaction commit */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5f2a4a7..3f9fe9d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2889,7 +2889,8 @@ typedef struct NotifyStmt
typedef struct ListenStmt
{
NodeTag type;
- char *conditionname; /* condition name to listen on */
+ bool isSimilarToPattern;
+ char *pattern; /* condition pattern to listen on */
} ListenStmt;
/* ----------------------
@@ -2899,7 +2900,7 @@ typedef struct ListenStmt
typedef struct UnlistenStmt
{
NodeTag type;
- char *conditionname; /* name to unlisten on, or NULL for all */
+ char *pattern; /* condition pattern to unlisten on, or NULL for all */
} UnlistenStmt;
/* ----------------------
diff --git a/src/test/isolation/expected/async-notify-2.out b/src/test/isolation/expected/async-notify-2.out
new file mode 100644
index 0000000..081cae3
--- /dev/null
+++ b/src/test/isolation/expected/async-notify-2.out
@@ -0,0 +1,118 @@
+Parsed test spec with 2 sessions
+
+starting permutation: listen_normal notify consume
+step listen_normal: LISTEN test;
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+
+starting permutation: listen_pattern_1 notify consume
+step listen_pattern_1: LISTEN SIMILAR TO 'te%';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+ASYNC NOTIFY of 'test_2' with payload '1' received
+ASYNC NOTIFY of 'test_2' with payload '2' received
+ASYNC NOTIFY of 'test_2' with payload '3' received
+ASYNC NOTIFY of 'test_2' with payload '4' received
+ASYNC NOTIFY of 'test_2' with payload '5' received
+
+starting permutation: listen_pattern_2 notify consume
+step listen_pattern_2: LISTEN SIMILAR TO 'test';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+
+starting permutation: listen_pattern_3 notify consume
+step listen_pattern_3: LISTEN SIMILAR TO 'te';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+
+starting permutation: listen_pattern_invalid notify consume
+step listen_pattern_invalid: LISTEN SIMILAR TO '*';
+ERROR: invalid regular expression: quantifier operand invalid
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+
+starting permutation: listen_normal listen_pattern_1 unlisten_1 notify consume
+step listen_normal: LISTEN test;
+step listen_pattern_1: LISTEN SIMILAR TO 'te%';
+step unlisten_1: UNLISTEN 't%';
+step notify:
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+
+count
+
+5
+count
+
+5
+step consume: BEGIN; END;
+ASYNC NOTIFY of 'test' with payload '1' received
+ASYNC NOTIFY of 'test' with payload '2' received
+ASYNC NOTIFY of 'test' with payload '3' received
+ASYNC NOTIFY of 'test' with payload '4' received
+ASYNC NOTIFY of 'test' with payload '5' received
+ASYNC NOTIFY of 'test_2' with payload '1' received
+ASYNC NOTIFY of 'test_2' with payload '2' received
+ASYNC NOTIFY of 'test_2' with payload '3' received
+ASYNC NOTIFY of 'test_2' with payload '4' received
+ASYNC NOTIFY of 'test_2' with payload '5' received
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 32c965b..a25cdeb 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -60,5 +60,6 @@ test: alter-table-3
test: create-trigger
test: sequence-ddl
test: async-notify
+test: async-notify-2
test: vacuum-reltuples
test: timeouts
diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c
index ba8082c..b1eb17d 100644
--- a/src/test/isolation/isolationtester.c
+++ b/src/test/isolation/isolationtester.c
@@ -46,7 +46,8 @@ static bool try_complete_step(Step *step, int flags);
static int step_qsort_cmp(const void *a, const void *b);
static int step_bsearch_cmp(const void *a, const void *b);
-static void printResultSet(PGresult *res);
+static void printResultSet(PGresult *res, PGconn *conn);
+static void printAsyncNotify(PGconn *conn);
/* close all connections and exit */
static void
@@ -487,7 +488,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[0], testspec->setupsqls[i]);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -505,7 +506,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[i + 1], testspec->sessions[i]->setupsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -640,7 +641,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[i + 1], testspec->sessions[i]->teardownsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -659,7 +660,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps)
res = PQexec(conns[0], testspec->teardownsql);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[0]);
}
else if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
@@ -821,9 +822,10 @@ try_complete_step(Step *step, int flags)
switch (PQresultStatus(res))
{
case PGRES_COMMAND_OK:
+ printAsyncNotify(conn);
break;
case PGRES_TUPLES_OK:
- printResultSet(res);
+ printResultSet(res, conn);
break;
case PGRES_FATAL_ERROR:
if (step->errormsg != NULL)
@@ -860,7 +862,7 @@ try_complete_step(Step *step, int flags)
}
static void
-printResultSet(PGresult *res)
+printResultSet(PGresult *res, PGconn *conn)
{
int nFields;
int i,
@@ -879,4 +881,24 @@ printResultSet(PGresult *res)
printf("%-15s", PQgetvalue(res, i, j));
printf("\n");
}
+
+ printAsyncNotify(conn);
+}
+
+static void
+printAsyncNotify(PGconn *conn)
+{
+ PGnotify *notify;
+
+ while ((notify = PQnotifies(conn)) != NULL)
+ {
+ if (notify->extra[0])
+ printf("ASYNC NOTIFY of '%s' with payload '%s' received\n",
+ notify->relname, notify->extra);
+ else
+ printf("ASYNC NOTIFY of '%s' received\n",
+ notify->relname);
+
+ PQfreemem(notify);
+ }
}
diff --git a/src/test/isolation/specs/async-notify-2.spec b/src/test/isolation/specs/async-notify-2.spec
new file mode 100644
index 0000000..dd1d38f
--- /dev/null
+++ b/src/test/isolation/specs/async-notify-2.spec
@@ -0,0 +1,31 @@
+# Verify that messages are consumed from the notify queue.
+
+session "listener"
+step "listen_normal" { LISTEN test; }
+step "listen_pattern_1" { LISTEN SIMILAR TO 'te%'; }
+step "listen_pattern_2" { LISTEN SIMILAR TO 'test'; }
+step "listen_pattern_3" { LISTEN SIMILAR TO 'te'; }
+step "listen_pattern_invalid" { LISTEN SIMILAR TO '*'; }
+step "unlisten_1" { UNLISTEN 't%'; }
+step "consume" { BEGIN; END; }
+teardown { UNLISTEN *; }
+
+session "notifier"
+step "notify"
+{
+ SELECT count(pg_notify('test', s::text)) FROM generate_series(1, 5) s;
+ SELECT count(pg_notify('test_2', s::text)) FROM generate_series(1, 5) s;
+}
+
+# Should print first notify channel
+permutation "listen_normal" "notify" "consume"
+# Should print both notify channels
+permutation "listen_pattern_1" "notify" "consume"
+# Should print first notify channel
+permutation "listen_pattern_2" "notify" "consume"
+# Should not print either notify channels
+permutation "listen_pattern_3" "notify" "consume"
+# Should fail to invalid RE pattern
+permutation "listen_pattern_invalid" "notify" "consume"
+# Test that UNLISTEN with a pattern does not work as a RE matcher
+permutation "listen_normal" "listen_pattern_1" "unlisten_1" "notify" "consume"
\ No newline at end of file
diff --git a/src/test/regress/expected/async.out b/src/test/regress/expected/async.out
index 19cbe38..963b1d0 100644
--- a/src/test/regress/expected/async.out
+++ b/src/test/regress/expected/async.out
@@ -27,11 +27,54 @@ SELECT pg_notify(NULL,'sample message1');
ERROR: channel name cannot be empty
SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
ERROR: channel name too long
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- src/test/isolation/specs/async-notify-2.spec tests for actual usage.
NOTIFY notify_async2;
LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_%';
UNLISTEN notify_async2;
+UNLISTEN 'notify_%';
+UNLISTEN 'notify_(%';
UNLISTEN *;
+-- Should fail. Invalid LISTEN command
+LISTEN *;
+ERROR: syntax error at or near "*"
+LINE 1: LISTEN *;
+ ^
+LISTEN notify_%;
+ERROR: syntax error at or near "%"
+LINE 1: LISTEN notify_%;
+ ^
+LISTEN SIMILAR TO 'notify_(%';
+ERROR: invalid regular expression: parentheses () not balanced
+LISTEN SIMILAR TO '*';
+ERROR: invalid regular expression: quantifier operand invalid
+-- Should contain two listeners
+LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_async2';
+LISTEN SIMILAR TO 'notify_%';
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+ notify_async2
+ notify_%
+(2 rows)
+
+-- Should contain one listener
+UNLISTEN 'notify_%';
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+ notify_async2
+(1 row)
+
+-- Should not contain listeners
+UNLISTEN *;
+SELECT pg_listening_channels();
+ pg_listening_channels
+-----------------------
+(0 rows)
+
-- Should return zero while there are no pending notifications.
-- src/test/isolation/specs/async-notify.spec tests for actual usage.
SELECT pg_notification_queue_usage();
diff --git a/src/test/regress/sql/async.sql b/src/test/regress/sql/async.sql
index 40f6e01..69d8f98 100644
--- a/src/test/regress/sql/async.sql
+++ b/src/test/regress/sql/async.sql
@@ -12,12 +12,36 @@ SELECT pg_notify('','sample message1');
SELECT pg_notify(NULL,'sample message1');
SELECT pg_notify('notify_async_channel_name_too_long______________________________','sample_message1');
---Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- Should work. Valid NOTIFY/LISTEN/UNLISTEN commands
+-- src/test/isolation/specs/async-notify-2.spec tests for actual usage.
NOTIFY notify_async2;
LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_%';
UNLISTEN notify_async2;
+UNLISTEN 'notify_%';
+UNLISTEN 'notify_(%';
UNLISTEN *;
+-- Should fail. Invalid LISTEN command
+LISTEN *;
+LISTEN notify_%;
+LISTEN SIMILAR TO 'notify_(%';
+LISTEN SIMILAR TO '*';
+
+-- Should contain two listeners
+LISTEN notify_async2;
+LISTEN SIMILAR TO 'notify_async2';
+LISTEN SIMILAR TO 'notify_%';
+SELECT pg_listening_channels();
+
+-- Should contain one listener
+UNLISTEN 'notify_%';
+SELECT pg_listening_channels();
+
+-- Should not contain listeners
+UNLISTEN *;
+SELECT pg_listening_channels();
+
-- Should return zero while there are no pending notifications.
-- src/test/isolation/specs/async-notify.spec tests for actual usage.
SELECT pg_notification_queue_usage();
Import Notes
Reply to msg id not found: CAMpj9JaSoWkUwJaifMqyDjQp21EROw9j5kWNp6rYyCkuERqg@mail.gmail.com
Hi Markus,
On Sun, Aug 20, 2017 at 9:56 PM, Markus Sintonen <markus.sintonen@gmail.com>
wrote:
I also encountered this when I built it with different configuration. I
attached updated patch with the correct number of arguments to
'similar_escape'. I also added preliminary documentation to the patch.
(Unfortunately unable to currently compile the documentation for testing
purpose on Windows probably because of commit https://github.com/post
gres/postgres/commit/510074f9f0131a04322d6a3d2a51c87e6db243f9. I followed
https://www.postgresql.org/docs/devel/static/install-windows
-full.html#idm45412738673840.)What do you think about the syntax? There was a suggestion to specify type
of the pattern (eg ltree extension) but to me this feels like a overkill.
One option here would be eg:
LISTEN PATTERN 'foo%' TYPE 'similar'
LISTEN PATTERN 'foo.*' TYPE 'ltree'
Not that my opinion matters, but I think we should pick one pattern style
and stick to it. SIMILAR TO doesn't seem like the worst choice. ltree
seems useless.
As for the rest of the interface..
First, I think mixing patterns and non-patterns is weird. This is apparent
in at least two cases:
marko=# listen "foo%";
LISTEN
marko=# listen similar to 'foo%';
LISTEN
marko=# select * from pg_listening_channels();
pg_listening_channels
-----------------------
foo%
(1 row)
-- Not actually listening on the pattern. Confusion.
The second case being the way UNLISTEN can be used to unlisten patterns,
too. It kind of makes sense given that you can't really end up with both a
channel name and a pattern with the same source string, but it's still
weird. I think it would be much better to keep these completely separate
so that you could be listening on both the channel "foo%" and the pattern
'foo%' at the same time, and you'd use a bare UNLISTEN to unsubscribe from
the former, and UNLISTEN SIMILAR TO for the latter. As you said in the
original email:
Allow *UNLISTEN SIMILAR TO 'xxx'* which would unregister matching
listeners. To me this feels confusing therefore it is not in the patch.
I agree, starting to match the patterns themselves would be confusing. So
I think we should use that syntax for unsubscribing from patterns. But
others should feel free to express their opinions on this.
Secondly -- and this is a kind of continuation to my previous point of
conflating patterns and non-patterns -- I don't think you can get away with
not changing the interface for pg_listening_channels(). Not knowing which
ones are compared byte-for-byte and which ones are patterns just seems
weird.
As for the patch itself, I have a couple of comments. I'm writing this
based on the latest commit in your git branch, commit
fded070f2a56024f931b9a0f174320eebc362458.
In queue_listen(), the new variables would be better declared at the
innermost scope possible. The datum is only used if isSimilarToPattern is
true, errormsg only if compile_regex didn't return REG_OKAY, etc..
I found this comment confusing at first: "If compiled RE was not applied as
a listener then it is freed at transaction commit." The past tense makes
it seem like something that has already happened when that code runs, when
in reality it happens later in the transaction.
I'm not a fan of the dance you're doing with pcompreg. I think it would be
better to optimistically allocate the ListenAction struct and compile
directly into actrec->compiledRegex.
The changed DEBUG1 line in Async_Listen should include whether it's a
pattern or not.
I don't understand why the return value of Exec_UnlistenAllCommit() was
changed at all. Why do we need to do something different based on whether
listenChannels was empty or not? The same goes for Exec_UnlistenCommit.
This looks wrong in isolationtester.c:
@@ -487,7 +488,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step
**steps)
res = PQexec(conns[0], testspec->setupsqls[i]);
if (PQresultStatus(res) == PGRES_TUPLES_OK)
{
- printResultSet(res);
+ printResultSet(res, conns[i + 1]);
(conns[0] vs. conns[i + 1]).
Moving to Waiting on Author.
.m
Hi Marko!
Thanks for the good feedback.
Good point on the pg_listening_channels(). Do you think we could change the
interface of the function? At least PG v10 has changed functions elsewhere
quite dramatically eg. related to xlog functions.
We could change the pg_listening_channels() return type to 'setof record'
which would include name (text) and isPattern (bool). This would also allow
some future additions (eg. time of last received notification). One other
way with better backward compatibility would be to add some kind of postfix
to patterns to identify them and keep the 'text' return type. It would
return eg. 'foo% - PATTERN' for a pattern, but this is quite nasty. Second
way would be to add totally new function eg. 'pg_listening_channels_info'
with return type of 'setof record' and keep the original function as it is
(just change the behaviour on duplicated names and patterns as you showed).
You also have a good point on the UNLISTEN. At first I thought that
UNLISTEN SIMILAR TO 'xxx' would be semantically confusing since it fells
that it would do some pattern based unregistering. But by documenting the
behaviour it might be ok. I also agree that it would be best to have
distinction between unlistening patterns and regular names.
I'll reflect on the rest of the feedback on the next patch if we reach some
conclusion on the pg_listening_channels and UNLISTEN.
BR
Markus
2017-09-08 2:44 GMT+03:00 Marko Tiikkaja <marko@joh.to>:
Show quoted text
Hi Markus,
On Sun, Aug 20, 2017 at 9:56 PM, Markus Sintonen <
markus.sintonen@gmail.com> wrote:I also encountered this when I built it with different configuration. I
attached updated patch with the correct number of arguments to
'similar_escape'. I also added preliminary documentation to the patch.
(Unfortunately unable to currently compile the documentation for testing
purpose on Windows probably because of commit https://github.com/post
gres/postgres/commit/510074f9f0131a04322d6a3d2a51c87e6db243f9. I
followed https://www.postgresql.org/docs/devel/static/install-windows
-full.html#idm45412738673840.)What do you think about the syntax? There was a suggestion to specify
type of the pattern (eg ltree extension) but to me this feels like a
overkill.
One option here would be eg:
LISTEN PATTERN 'foo%' TYPE 'similar'
LISTEN PATTERN 'foo.*' TYPE 'ltree'Not that my opinion matters, but I think we should pick one pattern style
and stick to it. SIMILAR TO doesn't seem like the worst choice. ltree
seems useless.As for the rest of the interface..
First, I think mixing patterns and non-patterns is weird. This is
apparent in at least two cases:marko=# listen "foo%";
LISTEN
marko=# listen similar to 'foo%';
LISTEN
marko=# select * from pg_listening_channels();
pg_listening_channels
-----------------------
foo%
(1 row)-- Not actually listening on the pattern. Confusion.
The second case being the way UNLISTEN can be used to unlisten patterns,
too. It kind of makes sense given that you can't really end up with both a
channel name and a pattern with the same source string, but it's still
weird. I think it would be much better to keep these completely separate
so that you could be listening on both the channel "foo%" and the pattern
'foo%' at the same time, and you'd use a bare UNLISTEN to unsubscribe from
the former, and UNLISTEN SIMILAR TO for the latter. As you said in the
original email:Allow *UNLISTEN SIMILAR TO 'xxx'* which would unregister matching
listeners. To me this feels confusing therefore it is not in the patch.
I agree, starting to match the patterns themselves would be confusing. So
I think we should use that syntax for unsubscribing from patterns. But
others should feel free to express their opinions on this.Secondly -- and this is a kind of continuation to my previous point of
conflating patterns and non-patterns -- I don't think you can get away with
not changing the interface for pg_listening_channels(). Not knowing which
ones are compared byte-for-byte and which ones are patterns just seems
weird.As for the patch itself, I have a couple of comments. I'm writing this
based on the latest commit in your git branch, commit
fded070f2a56024f931b9a0f174320eebc362458.In queue_listen(), the new variables would be better declared at the
innermost scope possible. The datum is only used if isSimilarToPattern is
true, errormsg only if compile_regex didn't return REG_OKAY, etc..I found this comment confusing at first: "If compiled RE was not applied
as a listener then it is freed at transaction commit." The past tense
makes it seem like something that has already happened when that code runs,
when in reality it happens later in the transaction.I'm not a fan of the dance you're doing with pcompreg. I think it would
be better to optimistically allocate the ListenAction struct and compile
directly into actrec->compiledRegex.The changed DEBUG1 line in Async_Listen should include whether it's a
pattern or not.I don't understand why the return value of Exec_UnlistenAllCommit() was
changed at all. Why do we need to do something different based on whether
listenChannels was empty or not? The same goes for Exec_UnlistenCommit.This looks wrong in isolationtester.c:
@@ -487,7 +488,7 @@ run_permutation(TestSpec *testspec, int nsteps, Step **steps) res = PQexec(conns[0], testspec->setupsqls[i]); if (PQresultStatus(res) == PGRES_TUPLES_OK) { - printResultSet(res); + printResultSet(res, conns[i + 1]);(conns[0] vs. conns[i + 1]).
Moving to Waiting on Author.
.m
On Sat, Sep 9, 2017 at 11:32 PM, Markus Sintonen
<markus.sintonen@gmail.com> wrote:
I'll reflect on the rest of the feedback on the next patch if we reach some
conclusion on the pg_listening_channels and UNLISTEN.
So this point is not settled? Still I can see that the feedback from
Marko has not been actually much answered, so I am marking it as
returned with feedback. The last patch fails to apply, the
documentation on HEAD produces I think warnings at compilation
(closing markups need to be fully-named).
--
Michael