Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wAlaU-000Ivg-0x for pgsql-hackers@arkaria.postgresql.org; Thu, 09 Apr 2026 09:22:11 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wAlaS-004qba-14 for pgsql-hackers@arkaria.postgresql.org; Thu, 09 Apr 2026 09:22:09 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wAlaR-004qbR-2x for pgsql-hackers@lists.postgresql.org; Thu, 09 Apr 2026 09:22:08 +0000 Received: from mail-pg1-x535.google.com ([2607:f8b0:4864:20::535]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wAlaP-000000008mJ-3qIG for pgsql-hackers@postgresql.org; Thu, 09 Apr 2026 09:22:07 +0000 Received: by mail-pg1-x535.google.com with SMTP id 41be03b00d2f7-b6ce6d1d3dcso288222a12.3 for ; Thu, 09 Apr 2026 02:22:06 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1775726525; cv=none; d=google.com; s=arc-20240605; b=Jv0k9dZTa5j00CuPZfVAoIRt2UImG3UnWo07BaXgqJOP4hAviusZB2169gfyBEVdzM SQkhk2/YH2b2P1fb51BMQV2mAsmpOftdQ4yV1uzkWdf0UfqoATTfRaVGBmYYhq8auGAh SDi0/yK4B1MiUNQlS/SyQwwYdW9pkKgjhrUhLru5eQujp9l4XaJvqBu4eCKfLRh57ziC XWWHL3GIM4hRF/eihnzJy++L8CGsOKLPdX2sgrvd4ofh6mBVrtnbmtG97SsD74u2ou5E rRFri7WtpbPCiSuO6PDPo3eS1vA560d/RU85/skvB2BJ9tuggzZDOJ/4SqB2kJeOaoV0 bQrw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=K+TJySYK/bdXdFoRHvJ1iyfRmDTMrg8lBoWwjRSG1aM=; fh=8P1oPqHgj0SBJbwlNo7mrpgRl+L+nfJlpLkallqq+Ko=; b=R1aGcs+k6L/Qx0EiB9MVCwgJHSmNXbKKRJOhFvHU2xzUjJPcqu/ajgsBs0665q64na oY+o22+o1YWMkFE+QIqfa9YswTOYPB4ZPm05xdVhNzgFSgVPJ9+gQP9EK2cgWUtz+T65 JSzl2YU2C81sC+mYTo0nh1PtKcxUrA1SELWPM68QG1X4Vy0qbb7NKaowa1sf+K6Rgu0b t2yxn+PtEiwPEB5b5PmwBsMfpv7ii6cLOH19OusxqsBRfi3U1GB0fKN6h/ziH8DP32YE uUF7RcZIsWy5vVD8+oeX0IW+vWbQqh5F/gWS4jQhvSgvu9fETO6wDBfXSCHhCwLVKOkJ Ubzw==; darn=postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1775726525; x=1776331325; darn=postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=K+TJySYK/bdXdFoRHvJ1iyfRmDTMrg8lBoWwjRSG1aM=; b=YdQ3sIMDXX8Uc1a63aTPUNPBsnQ1Gnvo42q5oEVdeshxMa7uqbl9Q+XQWNIPpM57kt 2bTiYQIw8q6MxegtQY3/9ckyQVBT57rNrxkjyVJDPGup3in9lGsZ0uSFAKI8G9hja3hb RkXNbRoDm0qrk56VD/8rhfwe8fByR6V4O6dHUmIn5ywgSeKR7l08D110b+wqAivGVDxk EweDKbqsd9zsZbSjdv2q/Ux4laW3kKxh8QLmeVOQG5pkDRW4C3zD0iiujDJgR2Cn8cqX ZNB3SFu3B8S1VFhD526ximKj+fZOT9KbklhVjZ+x3E2PdH6eK/46Hs9gYKgt+ULmjcNW MM6Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1775726525; x=1776331325; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=K+TJySYK/bdXdFoRHvJ1iyfRmDTMrg8lBoWwjRSG1aM=; b=RHDraeUX+7HwiKJiuJwR59KxD/FDioak70VpW4JIkvU0ZfOTLCJOVggFqLBH3hllmw 2SrhimnFJDAjM+mpNxieFDAUhZq8KgOiQyBJUmS3+TvQDQHSrJrH0radnWtqCmxT1h5s EoV15DqyYNZeJpv4Fp99wugeKPA4ELblkcEupp0DMnXMs2crmtXmqfptlEQty1A3M9Ub oSVQv/BuPNknHCI5qLtFqT9BRhuFz1bjcdM85Uhok4zdCbN/ilV7m6gk2sFeLPYt1Wzy 4/6OmFGZtG9asFeh/gXEjxM5wEg/8c2alhVqxxp85IrySxpA6uySF3yVgIHdw4AJIgVL EAPg== X-Forwarded-Encrypted: i=1; AJvYcCXl/aBblO6g5/lGDf/DUtdf9UEJeFLExygSwtmFCFV6yMDhX8evF0g/mF6uYrYkzSEhOEvKpdaWan1HrCS+@postgresql.org X-Gm-Message-State: AOJu0YyDZyFqYeWQwqmnnrQek7d7EKRNg/QkSpgdO0OcLdeLjHU7Cx9+ siL4QgJ47J5RqJVv4/TlVr/ThKn27sRE4HBCZzTtMM9EJylCRDTOEQDoLHgKoaCkketeaUaLAVw TDnubwb0Hq/V8uKWchBf64tN8mG2p004= X-Gm-Gg: AeBDietn/Eg0hciX5h5BNF6d4c/LYnqCvgrujmF7dxAeQF83rPfxSvxOVXf6JEwxGD3 ERDPboM5S0eRmgICx4IVh98IvcNH/AXBDoharjQZ8316ykcIQSeKGJ/fRHdcKWmIQw1ovD/rWWH e8QFA+uFpkuGaENTd+6O5XiYTkmauL+C7uY/litNL1909MTreBuz1urTOIFC3FXZUnJv5qiUSyd Q37mKsCMmlx0OjGTL1ARVsDee+xNwc1XrVk/mDilyw2Q2UB9ljM/XiyzGk7Kj4gCAHXl52Atrmo iiQbjFIz+6OzFx0Q30E/snfxC7EuUsgg X-Received: by 2002:a05:6a20:6a24:b0:39f:a42:924c with SMTP id adf61e73a8af0-39f2eff0334mr25703017637.17.1775726525189; Thu, 09 Apr 2026 02:22:05 -0700 (PDT) MIME-Version: 1.0 References: <2BE661BA-D909-4093-BF78-DB9B0C099337@gmail.com> <77FA04FE-1F84-4DA1-8855-8BBFD8CC889A@gmail.com> <72AA2663-B642-4FB1-BDC2-5FAFF2D2DF15@gmail.com> <8561287B-19F1-421E-959D-99F4593CFF54@gmail.com> In-Reply-To: From: jie wang Date: Thu, 9 Apr 2026 17:21:51 +0800 X-Gm-Features: AQROBzA1qpGLot9SkGwgcETbWeuUASOBmoWoodrjCCSVyIelRIgwM0hytPonCtc Message-ID: Subject: Re: Eliminating SPI / SQL from some RI triggers - take 3 To: Amit Langote Cc: Chao Li , Evan Montgomery-Recht , PostgreSQL-development Content-Type: multipart/alternative; boundary="000000000000c094ec064f038c37" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000c094ec064f038c37 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Amit Langote =E4=BA=8E2026=E5=B9=B44=E6=9C=889=E6= =97=A5=E5=91=A8=E5=9B=9B 16:41=E5=86=99=E9=81=93=EF=BC=9A > Hi, > > On Thu, Apr 9, 2026 at 4:40=E2=80=AFPM Chao Li w= rote: > > > On Apr 8, 2026, at 22:26, Amit Langote > wrote: > > > On Wed, Apr 8, 2026 at 6:58=E2=80=AFPM Amit Langote > wrote: > > >> On Wed, Apr 8, 2026 at 10:23=E2=80=AFAM Amit Langote > wrote: > > >>> On Tue, Apr 7, 2026 at 10:00=E2=80=AFPM Evan Montgomery-Recht > > >>> wrote: > > >>>> The patch also adds a test module (test_spi_func) with a C functio= n > > >>>> that executes SQL via SPI_connect/SPI_execute/SPI_finish, since th= is > > >>>> crash cannot be triggered from PL/pgSQL. The test exercises the > > >>>> C-level SPI INSERT with multiple FK constraints, FK violations, an= d > > >>>> nested PL/pgSQL-calls-C-SPI (matching the PostGIS call pattern). > > >> > > >> I applied only the test module changes and it passes (without > > >> crashing) even without your proposed fix. It seems that's because th= e > > >> C function in test_spi_func calling SPI is using the same resource > > >> owner as the parent SELECT. I think you'd need to create a resource > > >> owner manually in the spi_exec() C function to reproduce the crash, = as > > >> done in the attached 0001, which contains the src/test changes > > >> extracted from your patch modified as described, including renaming > > >> the C function to spi_exec_sql(). > > >> > > >> Also, the test cases that call spi_exec() (_sql()) directly from a > > >> SELECT don't actually exercise the crash path because there is no > > >> outer trigger-firing loop active. query_depth is 0 inside the inner > > >> SPI's AfterTriggerEndQuery, so the old guard wouldn't suppress the > > >> callback there anyway. The critical case requires spi_exec_sql() to = be > > >> called from inside an AFTER trigger, where query_depth > 0 causes th= e > > >> guard to defer the callback past the inner resource owner's lifetime= . > > >> I've added that test case. I kept your original test cases as they > > >> still provide useful coverage of C-level SPI FK behavior even if the= y > > >> don't exercise the crash path specifically. Maybe your original > > >> PostGIS test suite that hit the crash did have the right structure, > > >> but that's not reflected in the patch as far as I can tell. > > >> > > >> I've also renamed the module to test_spi_resowner to better reflect > > >> what it's about. > > >> > > >> For the fix, I have a different proposal. As you observed, the > > >> query_depth > 0 early return in FireAfterTriggerBatchCallbacks() mea= ns > > >> that the nested SPI's callbacks get called under the outer resource > > >> owner, which may not be the same as the one that SPI used. I think i= t > > >> was a mistake to have that early return in the first place. Instead = we > > >> could remember for each callback what firing level it should be call= ed > > >> at, so the nested SPI's callbacks fire before returning to the paren= t > > >> level and parent-level callbacks fire when the parent level complete= s. > > >> I have implemented that in the attached 0002 along with transaction > > >> boundary cleanup of callbacks, which passes the check-world for me, > > >> but I'll need to stare some more at it before committing. > > >> > > >> Let me know if this also fixes your own in-house test suite or if yo= u > > >> have any other suggestions or if you think I am missing something. > > > > > > One more cleanup patch attached as 0003: afterTriggerFiringDepth was > > > added by commit 5c54c3ed1 as a file-static variable, which in > > > hindsight should have been a field in AfterTriggersData alongside the > > > other per-transaction after-trigger state. This patch makes that > > > correction. > > > > > > One alternative design worth considering for 0002: storing > > > batch_callbacks per query level in AfterTriggersQueryData rather than > > > as a single list in AfterTriggersData, so callbacks naturally live at > > > the query level where they were registered and get cleaned up with > > > AfterTriggerFreeQuery on abort. Deferred constraints still need a > > > top-level list in AfterTriggersData since they fire outside any query > > > level. FireAfterTriggerBatchCallbacks() takes a list parameter and th= e > > > caller passes either the query-level or top-level list as appropriate= . > > > This eliminates the need for firing_depth-matched firing entirely. I > > > did that in 0004. I think I like it over 0002. Will look more > > > closely tomorrow morning. > > A few comments on v3: > > Thanks for the review. > > > 1 - 0002 > > ``` > > static void > > FireAfterTriggerBatchCallbacks(void) > > { > > + List *remaining =3D NIL; > > + List *to_fire =3D NIL; > > ListCell *lc; > > > > - if (afterTriggers.query_depth > 0) > > - return; > > + /* remaining and to_fire lists must survive until callbacks > complete */ > > + MemoryContext oldcxt =3D > MemoryContextSwitchTo(TopTransactionContext); > > ``` > > > > I think remaining and to_fire should stay in the same context of > afterTriggers.batch_callbacks, so instead of hard coding > TopTransactionContext, we can use > GetMemoryChunkContext(afterTriggers.batch_callbacks), which makes the > intention explicit. > > I'm dropping 0002 or have merged 0004 into it so this memory context > switch is no longer present. > > > 2 - 0004, I noticed one potential problem, although I am not sure > whether it can really happen in practice. This version stores callback > items at the individual query depth, and FireAfterTriggerBatchCallbacks() > now iterates the callback list for that depth and invokes each callback > directly. My concern is that if one of those callbacks needs to register = a > new callback, that would append a new item to the same list while it is > being iterated. That seems unsafe to me, because list append may create a > new list structure underneath. If that happens, we may end up modifying t= he > list being traversed, which does not look safe. > > > > This problem doesn=E2=80=99t exist in 0002, because 0002 splits > afterTriggers.batch_callbacks into remaining and to_fire, and reset > afterTriggers.batch_callbacks =3D remaining before running callbacks. But= the > problem is, if a callback registers a new callback, the new callback goes > to afterTriggers.batch_callbacks, so it won=E2=80=99t get executed. > > > > From this perspective, I would assume a callback should not be allowed > to register a new callback. Can you please help confirm? > > Good point on the re-entrant registration concern. I've added a > firing_batch_callbacks flag to AfterTriggersData that prevents > callbacks from registering new callbacks during > FireAfterTriggerBatchCallbacks(), with an Assert in > RegisterAfterTriggerBatchCallback() to enforce it. That should keep > the list being iterated from being modified. > > The attached patches are updated accordingly. 0001 is the main fix > incorporating the per-query-level storage design, the transaction > boundary cleanup, and the firing_batch_callbacks guard. 0002 is a > followup that moves afterTriggerFiringDepth into AfterTriggersData as > a minor cleanup of 5c54c3ed1b9. Barring further feedback I plan to > commit 0001 and 0002 shortly. For 0003, I need to check on the policy > around adding new test modules during feature freeze before committing > it. > > -- > Thanks, Amit Langote > Hi, I took a glance at the patch, overall looks good to me. A nitpick on 0001: + bool firing_batch_callbacks; /* true when in + * FireAfterTriggersBatchCallbacks() */ Looks like a typo in the comment. The function name is FireAfterTriggerBatchCallbacks, no =E2=80=9Cs=E2=80=9D after Trigger. Best regards, -- wang jie --000000000000c094ec064f038c37 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


Amit Langote &l= t;amitlangote09@gmail.com>= ; =E4=BA=8E2026=E5=B9=B44=E6=9C=889=E6=97=A5=E5=91=A8=E5=9B=9B 16:41=E5=86= =99=E9=81=93=EF=BC=9A
Hi,

On Thu, Apr 9, 2026 at 4:40=E2=80=AFPM Chao Li <li.evan.chao@gmail.com> wrote: > > On Apr 8, 2026, at 22:26, Amit Langote <amitlangote09@gmail.com> wrot= e:
> > On Wed, Apr 8, 2026 at 6:58=E2=80=AFPM Amit Langote <amitlangote09@gmail.c= om> wrote:
> >> On Wed, Apr 8, 2026 at 10:23=E2=80=AFAM Amit Langote <amitlangote09@gmai= l.com> wrote:
> >>> On Tue, Apr 7, 2026 at 10:00=E2=80=AFPM Evan Montgomery-R= echt
> >>> <montge@mianetworks.net> wrote:
> >>>> The patch also adds a test module (test_spi_func) wit= h a C function
> >>>> that executes SQL via SPI_connect/SPI_execute/SPI_fin= ish, since this
> >>>> crash cannot be triggered from PL/pgSQL. The test exe= rcises the
> >>>> C-level SPI INSERT with multiple FK constraints, FK v= iolations, and
> >>>> nested PL/pgSQL-calls-C-SPI (matching the PostGIS cal= l pattern).
> >>
> >> I applied only the test module changes and it passes (without=
> >> crashing) even without your proposed fix. It seems that's= because the
> >> C function in test_spi_func calling SPI is using the same res= ource
> >> owner as the parent SELECT. I think you'd need to create = a resource
> >> owner manually in the spi_exec() C function to reproduce the = crash, as
> >> done in the attached 0001, which contains the src/test change= s
> >> extracted from your patch modified as described, including re= naming
> >> the C function to spi_exec_sql().
> >>
> >> Also, the test cases that call spi_exec() (_sql()) directly f= rom a
> >> SELECT don't actually exercise the crash path because the= re is no
> >> outer trigger-firing loop active. query_depth is 0 inside the= inner
> >> SPI's AfterTriggerEndQuery, so the old guard wouldn't= suppress the
> >> callback there anyway. The critical case requires spi_exec_sq= l() to be
> >> called from inside an AFTER trigger, where query_depth > 0= causes the
> >> guard to defer the callback past the inner resource owner'= ;s lifetime.
> >> I've added that test case. I kept your original test case= s as they
> >> still provide useful coverage of C-level SPI FK behavior even= if they
> >> don't exercise the crash path specifically.=C2=A0 Maybe y= our original
> >> PostGIS test suite that hit the crash did have the right stru= cture,
> >> but that's not reflected in the patch as far as I can tel= l.
> >>
> >> I've also renamed the module to test_spi_resowner to bett= er reflect
> >> what it's about.
> >>
> >> For the fix, I have a different proposal. As you observed, th= e
> >> query_depth > 0 early return in FireAfterTriggerBatchCallb= acks() means
> >> that the nested SPI's callbacks get called under the oute= r resource
> >> owner, which may not be the same as the one that SPI used. I = think it
> >> was a mistake to have that early return in the first place. I= nstead we
> >> could remember for each callback what firing level it should = be called
> >> at, so the nested SPI's callbacks fire before returning t= o the parent
> >> level and parent-level callbacks fire when the parent level c= ompletes.
> >> I have implemented that in the attached 0002 along with trans= action
> >> boundary cleanup of callbacks, which passes the check-world f= or me,
> >> but I'll need to stare some more at it before committing.=
> >>
> >> Let me know if this also fixes your own in-house test suite o= r if you
> >> have any other suggestions or if you think I am missing somet= hing.
> >
> > One more cleanup patch attached as 0003: afterTriggerFiringDepth = was
> > added by commit 5c54c3ed1 as a file-static variable, which in
> > hindsight should have been a field in AfterTriggersData alongside= the
> > other per-transaction after-trigger state. This patch makes that<= br> > > correction.
> >
> > One alternative design worth considering for 0002: storing
> > batch_callbacks per query level in AfterTriggersQueryData rather = than
> > as a single list in AfterTriggersData, so callbacks naturally liv= e at
> > the query level where they were registered and get cleaned up wit= h
> > AfterTriggerFreeQuery on abort. Deferred constraints still need a=
> > top-level list in AfterTriggersData since they fire outside any q= uery
> > level. FireAfterTriggerBatchCallbacks() takes a list parameter an= d the
> > caller passes either the query-level or top-level list as appropr= iate.
> > This eliminates the need for firing_depth-matched firing entirely= . I
> > did that in 0004.=C2=A0 I think I like it over 0002.=C2=A0 Will l= ook more
> > closely tomorrow morning.
> A few comments on v3:

Thanks for the review.

> 1 - 0002
> ```
>=C2=A0 static void
>=C2=A0 FireAfterTriggerBatchCallbacks(void)
>=C2=A0 {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0List=C2=A0 =C2=A0 =C2=A0 =C2=A0*remaining = =3D NIL;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0List=C2=A0 =C2=A0 =C2=A0 =C2=A0*to_fire = =3D NIL;
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0ListCell=C2=A0 =C2=A0*lc;
>
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0if (afterTriggers.query_depth > 0)
> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return;
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0/* remaining and to_fire lists must surviv= e until callbacks complete */
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0MemoryContext oldcxt =3D MemoryContextSwit= chTo(TopTransactionContext);
> ```
>
> I think remaining and to_fire should stay in the same context of after= Triggers.batch_callbacks, so instead of hard coding TopTransactionContext, = we can use GetMemoryChunkContext(afterTriggers.batch_callbacks), which make= s the intention explicit.

I'm dropping 0002 or have merged 0004 into it so this memory context switch is no longer present.

> 2 - 0004, I noticed one potential problem, although I am not sure whet= her it can really happen in practice. This version stores callback items at= the individual query depth, and FireAfterTriggerBatchCallbacks() now itera= tes the callback list for that depth and invokes each callback directly. My= concern is that if one of those callbacks needs to register a new callback= , that would append a new item to the same list while it is being iterated.= That seems unsafe to me, because list append may create a new list structu= re underneath. If that happens, we may end up modifying the list being trav= ersed, which does not look safe.
>
> This problem doesn=E2=80=99t exist in 0002, because 0002 splits afterT= riggers.batch_callbacks into remaining and to_fire, and reset afterTriggers= .batch_callbacks =3D remaining before running callbacks. But the problem is= , if a callback registers a new callback, the new callback goes to afterTri= ggers.batch_callbacks, so it won=E2=80=99t get executed.
>
> From this perspective, I would assume a callback should not be allowed= to register a new callback. Can you please help confirm?

Good point on the re-entrant registration concern. I've added a
firing_batch_callbacks flag to AfterTriggersData that prevents
callbacks from registering new callbacks during
FireAfterTriggerBatchCallbacks(), with an Assert in
RegisterAfterTriggerBatchCallback() to enforce it. That should keep
the list being iterated from being modified.

The attached patches are updated accordingly. 0001 is the main fix
incorporating the per-query-level storage design, the transaction
boundary cleanup, and the firing_batch_callbacks guard. 0002 is a
followup that moves afterTriggerFiringDepth into AfterTriggersData as
a minor cleanup of 5c54c3ed1b9. Barring further feedback I plan to
commit 0001 and 0002 shortly. For 0003, I need to check on the policy
around adding new test modules during feature freeze before committing
it.

--
Thanks, Amit Langote


=C2= =A0Hi,

I took a glance at the patch, overall looks good to me. A n= itpick on 0001:

+ =C2=A0 =C2=A0 =C2=A0 bool =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0firing_batch_callbacks; /* true when in
+=C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0* FireAfterTriggersBatchCallba= cks() */

Looks like a typo in the comment. The function name is Fire= AfterTriggerBatchCallbacks, no =E2=80=9Cs=E2=80=9D after Trigger.

Be= st regards,
--
wang jie
--000000000000c094ec064f038c37--