Thread

Commits

  1. Fix data loss when restarting the bulk_write facility

  2. Introduce a new smgr bulk loading facility.

  1. Changed behavior in rewriteheap

    Erik Nordström <erik@timescale.com> — 2024-11-21T16:08:49Z

    Hello,
    
    I've noticed a change in behavior of the heap rewrite functionality in
    PostgreSQL 17, used by, e.g., CLUSTER. I've been experimenting with the
    functionality to implement a way to merge partitions in TimescaleDB. I am
    using table_relation_copy_for_cluster() to write the data of several tables
    to a single merged table, and then I do a heap swap on one of the original
    tables while dropping the others. So, if you have 3 partitions and want to
    merge them to one, then I write all three partitions to a temporary heap,
    swap the new heap on partition 1 and then drop partitions 2 and 3.
    
    Now, this approach worked fine for PostgreSQL 15 and 16, but 17 introduced
    some changes that altered the behavior so that I only see data from one of
    the partitions after merge (the last one written).
    
    The commit that I think is responsible is the following:
    https://github.com/postgres/postgres/commit/8af256524893987a3e534c6578dd60edfb782a77
    
    Now, I realize that this is not a problem for PostgreSQL itself since the
    rewrite functionality isn't used for the purpose I am using it. To my
    defense, the rewrite code seems to imply that it should be possible to
    write more data to an existing heap according to this comment in
    begin_heap_rewrite: /* new_heap needn't be empty, just locked */.
    
    I've also tried recompiling PG17 with the rewriteheap.c file from PG16 and
    then it works again. I haven't yet been able to figure out exactly what is
    different but I will continue to try to narrow it down. In the meantime,
    maybe someone on the mailing list has some insight on what could be the
    issue and whether my approach is viable?
    
    Regards,
    Erik
    
    -- 
    Database Architect, Timescale
    
  2. Re: Changed behavior in rewriteheap

    Matthias van de Meent <boekewurm+postgres@gmail.com> — 2024-11-21T23:30:14Z

    On Thu, 21 Nov 2024, 17:18 Erik Nordström, <erik@timescale.com> wrote:
    
    > Hello,
    >
    > I've noticed a change in behavior of the heap rewrite functionality in
    > PostgreSQL 17, used by, e.g., CLUSTER. I've been experimenting with the
    > functionality to implement a way to merge partitions in TimescaleDB. I am
    > using table_relation_copy_for_cluster() to write the data of several tables
    > to a single merged table, and then I do a heap swap on one of the original
    > tables while dropping the others. So, if you have 3 partitions and want to
    > merge them to one, then I write all three partitions to a temporary heap,
    > swap the new heap on partition 1 and then drop partitions 2 and 3.
    >
    > Now, this approach worked fine for PostgreSQL 15 and 16, but 17 introduced
    > some changes that altered the behavior so that I only see data from one of
    > the partitions after merge (the last one written).
    >
    > The commit that I think is responsible is the following:
    > https://github.com/postgres/postgres/commit/8af256524893987a3e534c6578dd60edfb782a77
    >
    > Now, I realize that this is not a problem for PostgreSQL itself since the
    > rewrite functionality isn't used for the purpose I am using it. To my
    > defense, the rewrite code seems to imply that it should be possible to
    > write more data to an existing heap according to this comment in
    > begin_heap_rewrite: /* new_heap needn't be empty, just locked */.
    >
    > I've also tried recompiling PG17 with the rewriteheap.c file from PG16 and
    > then it works again. I haven't yet been able to figure out exactly what is
    > different but I will continue to try to narrow it down. In the meantime,
    > maybe someone on the mailing list has some insight on what could be the
    > issue and whether my approach is viable?
    >
    
    I think the origin of the issue is at bulk_write.c, in smgr_bulk_flush
    (specifically after line 282), which assumes the bulk write system is used
    exclusively on empty relations.
    
    If you use a separate pair of begin/end_heap_rewrite for every relation you
    merge into that heap, it'll re-initialize the bulk writer, which will thus
    overwrite the previous rewrites' pages. The pre-PG17 rewriteheap.c doesn't
    use that API, and thus isn't affected.
    
    I've CC-ed Heikki as author of that patch; maybe a new API to indicate bulk
    writes into an existing fork would make sense, to solve Timescale's issue
    and fix rewriteheap's behavior?
    
    
    Kind regards,
    
    Matthias van de Meent
    Neon (https://neon.tech)
    
  3. Re: Changed behavior in rewriteheap

    Erik Nordström <erik@timescale.com> — 2024-11-22T08:11:21Z

    On Fri, Nov 22, 2024 at 12:30 AM Matthias van de Meent <
    boekewurm+postgres@gmail.com> wrote:
    
    > On Thu, 21 Nov 2024, 17:18 Erik Nordström, <erik@timescale.com> wrote:
    >
    >> Hello,
    >>
    >> I've noticed a change in behavior of the heap rewrite functionality in
    >> PostgreSQL 17, used by, e.g., CLUSTER. I've been experimenting with the
    >> functionality to implement a way to merge partitions in TimescaleDB. I am
    >> using table_relation_copy_for_cluster() to write the data of several tables
    >> to a single merged table, and then I do a heap swap on one of the original
    >> tables while dropping the others. So, if you have 3 partitions and want to
    >> merge them to one, then I write all three partitions to a temporary heap,
    >> swap the new heap on partition 1 and then drop partitions 2 and 3.
    >>
    >> Now, this approach worked fine for PostgreSQL 15 and 16, but 17
    >> introduced some changes that altered the behavior so that I only see data
    >> from one of the partitions after merge (the last one written).
    >>
    >> The commit that I think is responsible is the following:
    >> https://github.com/postgres/postgres/commit/8af256524893987a3e534c6578dd60edfb782a77
    >>
    >> Now, I realize that this is not a problem for PostgreSQL itself since the
    >> rewrite functionality isn't used for the purpose I am using it. To my
    >> defense, the rewrite code seems to imply that it should be possible to
    >> write more data to an existing heap according to this comment in
    >> begin_heap_rewrite: /* new_heap needn't be empty, just locked */.
    >>
    >> I've also tried recompiling PG17 with the rewriteheap.c file from PG16
    >> and then it works again. I haven't yet been able to figure out exactly what
    >> is different but I will continue to try to narrow it down. In the meantime,
    >> maybe someone on the mailing list has some insight on what could be the
    >> issue and whether my approach is viable?
    >>
    >
    > I think the origin of the issue is at bulk_write.c, in smgr_bulk_flush
    > (specifically after line 282), which assumes the bulk write system is used
    > exclusively on empty relations.
    >
    > If you use a separate pair of begin/end_heap_rewrite for every relation
    > you merge into that heap, it'll re-initialize the bulk writer, which will
    > thus overwrite the previous rewrites' pages. The pre-PG17 rewriteheap.c
    > doesn't use that API, and thus isn't affected.
    >
    > I've CC-ed Heikki as author of that patch; maybe a new API to indicate
    > bulk writes into an existing fork would make sense, to solve Timescale's
    > issue and fix rewriteheap's behavior?
    >
    >
    > Kind regards,
    >
    > Matthias van de Meent
    > Neon (https://neon.tech)
    >
    
    Yes, looks like it is that code that zeros out the initial pages if the
    write doesn't start at block 0. And it looks like I don't have access to
    the bulk write state to set pages_written to trick it to believe those
    pages have already been written. Maybe it is possible to add an option to
    the bulkwriter to skip zero-initialization? The comment for that code seems
    to indicate it isn't strictly necessary anyway.
    
    Regards,
    
    Erik
    
  4. Re: Changed behavior in rewriteheap

    Matthias van de Meent <boekewurm+postgres@gmail.com> — 2024-11-22T13:02:08Z

    On Fri, 22 Nov 2024 at 09:11, Erik Nordström <erik@timescale.com> wrote:
    >
    >
    >
    > On Fri, Nov 22, 2024 at 12:30 AM Matthias van de Meent <boekewurm+postgres@gmail.com> wrote:
    >>
    >> On Thu, 21 Nov 2024, 17:18 Erik Nordström, <erik@timescale.com> wrote:
    >>>
    >>> Hello,
    >>>
    >>> I've noticed a change in behavior of the heap rewrite functionality in PostgreSQL 17, used by, e.g., CLUSTER. I've been experimenting with the functionality to implement a way to merge partitions in TimescaleDB. I am using table_relation_copy_for_cluster() to write the data of several tables to a single merged table, and then I do a heap swap on one of the original tables while dropping the others. So, if you have 3 partitions and want to merge them to one, then I write all three partitions to a temporary heap, swap the new heap on partition 1 and then drop partitions 2 and 3.
    >>>
    >>> Now, this approach worked fine for PostgreSQL 15 and 16, but 17 introduced some changes that altered the behavior so that I only see data from one of the partitions after merge (the last one written).
    [...]
    >>> I've also tried recompiling PG17 with the rewriteheap.c file from PG16 and then it works again. I haven't yet been able to figure out exactly what is different but I will continue to try to narrow it down. In the meantime, maybe someone on the mailing list has some insight on what could be the issue and whether my approach is viable?
    >>
    >>
    >> I think the origin of the issue is at bulk_write.c, in smgr_bulk_flush (specifically after line 282), which assumes the bulk write system is used exclusively on empty relations.
    >>
    >> If you use a separate pair of begin/end_heap_rewrite for every relation you merge into that heap, it'll re-initialize the bulk writer, which will thus overwrite the previous rewrites' pages. The pre-PG17 rewriteheap.c doesn't use that API, and thus isn't affected.
    >>
    >
    >
    > Yes, looks like it is that code that zeros out the initial pages if the write doesn't start at block 0. And it looks like I don't have access to the bulk write state to set pages_written to trick it to believe those pages have already been written. Maybe it is possible to add an option to the bulkwriter to skip zero-initialization? The comment for that code seems to indicate it isn't strictly necessary anyway.
    
    I think I'd go with a patch like attached, where the bulk writer
    registers that it started with .relsize pages in the relfork, and use
    that for smgrextend() decisions. It now also records pages_written as
    a separate but accurate value.
    
    Side note: Interestingly, on a replica you would see all relation's
    data in that table, or at least until the primary writes new FPIs for
    those pages, as the smgrextend() which clears the pages isn't
    WAL-logged and thus doesn't get transfered to a replica.
    
    Kind regards,
    
    Matthias van de Meent
    
  5. Re: Changed behavior in rewriteheap

    Heikki Linnakangas <hlinnaka@iki.fi> — 2024-11-22T13:26:56Z

    On 22/11/2024 15:02, Matthias van de Meent wrote:
    > I think I'd go with a patch like attached, where the bulk writer
    > registers that it started with .relsize pages in the relfork, and use
    > that for smgrextend() decisions. It now also records pages_written as
    > a separate but accurate value.
    
    Looks good to me. Eric, can you confirm that Matthias's patch fixes the 
    problem for you?
    
    -- 
    Heikki Linnakangas
    Neon (https://neon.tech)
    
    
    
    
    
  6. Re: Changed behavior in rewriteheap

    Erik Nordström <erik@timescale.com> — 2024-11-22T13:56:36Z

    On Fri, Nov 22, 2024 at 2:26 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
    
    > On 22/11/2024 15:02, Matthias van de Meent wrote:
    > > I think I'd go with a patch like attached, where the bulk writer
    > > registers that it started with .relsize pages in the relfork, and use
    > > that for smgrextend() decisions. It now also records pages_written as
    > > a separate but accurate value.
    >
    > Looks good to me. Eric, can you confirm that Matthias's patch fixes the
    > problem for you?
    >
    >
    Yes, it solves the issue so it looks good.
    
    Just a minor nit: the code uses both blokno as local variable for
    pending_writes[i].blkno and directly accessing pending_writes[i].blkno.
    Maybe it is better to just use the local variable. For example, change
    
    ++ b/src/backend/storage/smgr/bulk_write.c
    @@ -304,7 +304,8 @@ smgr_bulk_flush(BulkWriteState *bulkstate)
                            }
    
                            smgrextend(bulkstate->smgr, bulkstate->forknum,
    blkno, page, true);
    -                       bulkstate->relsize = pending_writes[i].blkno + 1;
    +                       bulkstate->relsize++;
    +                       Assert(bulkstate->relsize == blkno + 1);
    
    Just a suggestion.
    
    Thanks for the quick action!
    
    -Erik
    
  7. Re: Changed behavior in rewriteheap

    Heikki Linnakangas <hlinnaka@iki.fi> — 2024-11-22T14:53:00Z

    On 22/11/2024 15:56, Erik Nordström wrote:
    > Yes, it solves the issue so it looks good.
    > 
    > Just a minor nit: the code uses both blokno as local variable for 
    > pending_writes[i].blkno and directly accessing pending_writes[i].blkno. 
    > Maybe it is better to just use the local variable. For example, change
    > 
    > ++ b/src/backend/storage/smgr/bulk_write.c
    > @@ -304,7 +304,8 @@ smgr_bulk_flush(BulkWriteState *bulkstate)
    >                          }
    > 
    >                          smgrextend(bulkstate->smgr, bulkstate->forknum, 
    > blkno, page, true);
    > -                       bulkstate->relsize = pending_writes[i].blkno + 1;
    > +                       bulkstate->relsize++;
    > +                       Assert(bulkstate->relsize == blkno + 1);
    > 
    > Just a suggestion.
    
    Made that change and committed to master and REL_17_STABLE. I didn't 
    bother with the assertion though. Also I removed the 'pages_written' 
    field, it was not used for anything anymore.
    
    Thanks!
    
    -- 
    Heikki Linnakangas
    Neon (https://neon.tech)
    
    
    
    
    
  8. Re: Changed behavior in rewriteheap

    Erik Nordström <erik@timescale.com> — 2024-11-22T15:02:13Z

    On Fri, Nov 22, 2024 at 3:53 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
    
    > On 22/11/2024 15:56, Erik Nordström wrote:
    > > Yes, it solves the issue so it looks good.
    > >
    > > Just a minor nit: the code uses both blokno as local variable for
    > > pending_writes[i].blkno and directly accessing pending_writes[i].blkno.
    > > Maybe it is better to just use the local variable. For example, change
    > >
    > > ++ b/src/backend/storage/smgr/bulk_write.c
    > > @@ -304,7 +304,8 @@ smgr_bulk_flush(BulkWriteState *bulkstate)
    > >                          }
    > >
    > >                          smgrextend(bulkstate->smgr, bulkstate->forknum,
    > > blkno, page, true);
    > > -                       bulkstate->relsize = pending_writes[i].blkno + 1;
    > > +                       bulkstate->relsize++;
    > > +                       Assert(bulkstate->relsize == blkno + 1);
    > >
    > > Just a suggestion.
    >
    > Made that change and committed to master and REL_17_STABLE. I didn't
    > bother with the assertion though. Also I removed the 'pages_written'
    > field, it was not used for anything anymore.
    >
    
    Sounds good. Thank you again!
    
    -Erik