Re: On login trigger: take three

From: Andres Freund <andres(at)anarazel(dot)de>
To: Alexander Korotkov <aekorotkov(at)gmail(dot)com>
Cc: Robert Haas <robertmhaas(at)gmail(dot)com>, Daniel Gustafsson <daniel(at)yesql(dot)se>, Mikhail Gribkov <youzhick(at)gmail(dot)com>, Tom Lane <tgl(at)sss(dot)pgh(dot)pa(dot)us>, Greg Nancarrow <gregn4422(at)gmail(dot)com>, Ivan Panchenko <wao(at)mail(dot)ru>, Teodor Sigaev <teodor(at)sigaev(dot)ru>, Ibrar Ahmed <ibrar(dot)ahmad(at)gmail(dot)com>, vignesh C <vignesh21(at)gmail(dot)com>, Pavel Stehule <pavel(dot)stehule(at)gmail(dot)com>, PostgreSQL Hackers <pgsql-hackers(at)postgresql(dot)org>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, Masahiko Sawada <sawada(dot)mshk(at)gmail(dot)com>
Subject: Re: On login trigger: take three
Date: 2023-10-10 16:37:30
Message-ID: 20231010163730.qwadzt5f3biprook@awork3.anarazel.de
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

On 2023-10-10 08:18:46 +0300, Alexander Korotkov wrote:
> @@ -968,7 +969,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
>
> if (!get_db_info(dbtemplate, ShareLock,
> &src_dboid, &src_owner, &src_encoding,
> - &src_istemplate, &src_allowconn,
> + &src_istemplate, &src_allowconn, &src_hasloginevt,
> &src_frozenxid, &src_minmxid, &src_deftablespace,
> &src_collate, &src_ctype, &src_iculocale, &src_icurules, &src_locprovider,
> &src_collversion))

This isn't your fault, but this imo has become unreadable. Think we ought to
move the information about a database to a struct.

> @@ -296,6 +306,13 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO
> CatalogTupleInsert(tgrel, tuple);
> heap_freetuple(tuple);
>
> + /*
> + * Login event triggers have an additional flag in pg_database to allow
> + * faster lookups in hot codepaths. Set the flag unless already True.
> + */
> + if (strcmp(eventname, "login") == 0)
> + SetDatatabaseHasLoginEventTriggers();

It's not really faster lookups, it's no lookups, right?

> /* Depend on owner. */
> recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
>
> @@ -357,6 +374,39 @@ filter_list_to_array(List *filterlist)
> return PointerGetDatum(construct_array_builtin(data, l, TEXTOID));
> }
>
> +/*
> + * Set pg_database.dathasloginevt flag for current database indicating that
> + * current database has on login triggers.
> + */
> +void
> +SetDatatabaseHasLoginEventTriggers(void)
> +{
> + /* Set dathasloginevt flag in pg_database */
> + Form_pg_database db;
> + Relation pg_db = table_open(DatabaseRelationId, RowExclusiveLock);
> + HeapTuple tuple;
> +
> + /*
> + * Use shared lock to prevent a conflit with EventTriggerOnLogin() trying
> + * to reset pg_database.dathasloginevt flag. Note that we use
> + * AccessShareLock allowing setters concurently.
> + */
> + LockSharedObject(DatabaseRelationId, MyDatabaseId, 0, AccessShareLock);

That seems like a very odd approach - how does this avoid concurrency issues
with one backend setting and another unsetting the flag? And outside of that,
won't this just lead to concurrently updated tuples?

> + tuple = SearchSysCacheCopy1(DATABASEOID, ObjectIdGetDatum(MyDatabaseId));
> + if (!HeapTupleIsValid(tuple))
> + elog(ERROR, "cache lookup failed for database %u", MyDatabaseId);
> + db = (Form_pg_database) GETSTRUCT(tuple);
> + if (!db->dathasloginevt)
> + {
> + db->dathasloginevt = true;
> + CatalogTupleUpdate(pg_db, &tuple->t_self, tuple);
> + CommandCounterIncrement();
> + }
> + table_close(pg_db, RowExclusiveLock);
> + heap_freetuple(tuple);
> +}
> +
> /*
> * ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
> */
> @@ -391,6 +441,10 @@ AlterEventTrigger(AlterEventTrigStmt *stmt)
>
> CatalogTupleUpdate(tgrel, &tup->t_self, tup);
>
> + if (namestrcmp(&evtForm->evtevent, "login") == 0 &&
> + tgenabled != TRIGGER_DISABLED)
> + SetDatatabaseHasLoginEventTriggers();
> +
> InvokeObjectPostAlterHook(EventTriggerRelationId,
> trigoid, 0);
>
> @@ -557,7 +611,7 @@ filter_event_trigger(CommandTag tag, EventTriggerCacheItem *item)
> static List *
> EventTriggerCommonSetup(Node *parsetree,
> EventTriggerEvent event, const char *eventstr,
> - EventTriggerData *trigdata)
> + EventTriggerData *trigdata, bool unfiltered)
> {
> CommandTag tag;
> List *cachelist;
> @@ -582,10 +636,15 @@ EventTriggerCommonSetup(Node *parsetree,
> {
> CommandTag dbgtag;
>
> - dbgtag = CreateCommandTag(parsetree);
> + if (event == EVT_Login)
> + dbgtag = CMDTAG_LOGIN;
> + else
> + dbgtag = CreateCommandTag(parsetree);
> +
> if (event == EVT_DDLCommandStart ||
> event == EVT_DDLCommandEnd ||
> - event == EVT_SQLDrop)
> + event == EVT_SQLDrop ||
> + event == EVT_Login)
> {
> if (!command_tag_event_trigger_ok(dbgtag))
> elog(ERROR, "unexpected command tag \"%s\"", GetCommandTagName(dbgtag));
> @@ -604,7 +663,10 @@ EventTriggerCommonSetup(Node *parsetree,
> return NIL;
>
> /* Get the command tag. */
> - tag = CreateCommandTag(parsetree);
> + if (event == EVT_Login)
> + tag = CMDTAG_LOGIN;
> + else
> + tag = CreateCommandTag(parsetree);

Seems this bit should instead be in a function, given that you have it in
multiple places.

>
> +/*
> + * Fire login event triggers if any are present. The dathasloginevt
> + * pg_database flag is left when an event trigger is dropped, to avoid
> + * complicating the codepath in the case of multiple event triggers. This
> + * function will instead unset the flag if no trigger is defined.
> + */
> +void
> +EventTriggerOnLogin(void)
> +{
> + List *runlist;
> + EventTriggerData trigdata;
> +
> + /*
> + * See EventTriggerDDLCommandStart for a discussion about why event
> + * triggers are disabled in single user mode or via a GUC. We also need a
> + * database connection (some background workers doesn't have it).
> + */
> + if (!IsUnderPostmaster || !event_triggers ||
> + !OidIsValid(MyDatabaseId) || !MyDatabaseHasLoginEventTriggers)
> + return;
> +
> + StartTransactionCommand();
> + runlist = EventTriggerCommonSetup(NULL,
> + EVT_Login, "login",
> + &trigdata, false);
> +
> + if (runlist != NIL)
> + {
> + /*
> + * Event trigger execution may require an active snapshot.
> + */
> + PushActiveSnapshot(GetTransactionSnapshot());
> +
> + /* Run the triggers. */
> + EventTriggerInvoke(runlist, &trigdata);
> +
> + /* Cleanup. */
> + list_free(runlist);
> +
> + PopActiveSnapshot();
> + }
> + /*
> + * There is no active login event trigger, but our pg_database.dathasloginevt was set.
> + * Try to unset this flag. We use the lock to prevent concurrent
> + * SetDatatabaseHasLoginEventTriggers(), but we don't want to hang the
> + * connection waiting on the lock. Thus, we are just trying to acquire
> + * the lock conditionally.
> + */
> + else if (ConditionalLockSharedObject(DatabaseRelationId, MyDatabaseId,
> + 0, AccessExclusiveLock))

Eek. Why are we doing it this way? I think this is a seriously bad
idea. Maybe it's obvious to you, but it seems much more reasonable to make the
pg_database column an integer and count the number of login event
triggers. When 0, then we don't need to look for login event triggers.

Greetings,

Andres Freund

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message jinser 2023-10-10 17:11:15 Fix typo in psql zh_CN.po
Previous Message Alvaro Herrera 2023-10-10 15:15:36 Re: Fwd: Advice about preloaded libraries