0

Pre-requisites

Postgresql

Spring boot with spring data jpa

Problem

I have 2 tables. Products and ProductsLocationCounter. Each product has a location_id and counter_value fields among others. location_id is also the primary key of ProductsLocationCounter. The ProductsLocationCounter is meant to keep a counter of the number of products grouped by a specific location_id whenever a new product is added. The problem is that I also need the counter value at that point in time to be attached to the product entity.

So the flow would be like

 1. create product
 2. counter_value = get counter
 3. increment counter
 4. product.counter_value = counter_value

Of course this has to be done in a concurrent matter. Now, i've read/tried different solutions.

  1. this stackoverflow post suggests that i should let the db to handle the concurrency, which sounds fine by me. But the trick is that I need the value of the counter in the same transaction. So I've created a trigger
CREATE FUNCTION maintain_location_product_count_fun() RETURNS TRIGGER AS
$$
DECLARE
counter_var BIGINT;
BEGIN
  IF TG_OP IN ('INSERT') THEN
    select product_location_count.counter into counter_var from product_location_count WHERE id = new.location_id FOR UPDATE;
    UPDATE product_location_count SET counter = counter + 1 WHERE id = new.location_id;
    UPDATE products SET counter_value = counter_var WHERE location_id = new.location_id;
  END IF;
  RETURN NULL;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER maintain_location_product_count_trig
AFTER INSERT ON products
FOR EACH ROW
EXECUTE PROCEDURE maintain_location_product_count_fun();

and tested it with a parallel stream

    IntStream.range(1, 5000)
        .parallel()
        .forEach(value -> {
          executeInsideTransactionTemplate(status -> {
            var location = locationRepository.findById(location.getId()).get();
            return addProductWithLocation(location)
          });
        });

Got no duplication on the counter_value column. Is this trigger safe for multi-threaded apps? Haven't worked with triggers/postgresql functions before. Not sure what to expect


  1. The second solution I tried was to add PESIMISTIC_WRITE on findById method of the ProductsLocationCounter entity but i ended up getting cannot execute SELECT FOR UPDATE in a read-only transaction even though i was executing the code in a @Transactional annotated method ( which by default has read-only false).

  2. The third one was to update and retrieve the value of the counter in the same statement but spring jpa doesn't allow that (nor the underlying db) as the update statement only return the number of rows affected

Is there any other solution or do i need to add something to the trigger function to make it threadsafe? Thank you

ALex
  • 673
  • 1
  • 6
  • 19
  • trigger seems to be OK (I would prefer to remove first select ... for update in favour of returning into in update stmt. though, and you actually need to create three triggers), however if product_location_count table is managed by HBN and it is not marked as @Immutable (or counter column is not marked as non-updatable), you may potentially loose some updates – Andrey B. Panfilov Oct 25 '22 at 11:28
  • why 3 triggers? – ALex Oct 25 '22 at 11:39
  • If I understand properly you need to keep value of `products.counter_value` in sync with `product_location_count.counter`, so, you need three triggers: one for insert, one for delete and one for update. – Andrey B. Panfilov Oct 25 '22 at 11:42
  • No, i don't need it to be synced. I need to get the atomic value of `product_location_count.counter` at moment of time t1, add the value to the product, update the `product_location_count.counter = product_location_count.counter + 1` then release it for another thread/client to come pick up the next value – ALex Oct 25 '22 at 11:59
  • Before digging into the technical details I would suggest to think what these counters are for from the business perspective. Could you update the post, please, and provide more details? What `product.counter` is used for? – Andriy Slobodyanyk Oct 25 '22 at 14:25
  • thx @AndriySlobodyanyk but i've explained the business logic already. I've found a solution anyway. – ALex Oct 26 '22 at 12:51
  • I only want to emphasize that the problem looks like a typical counter. It is fine but on the other hand it makes a bottleneck, all product creation requests could be handled serially only, couldn't be scaled horizontally and so on. – Andriy Slobodyanyk Oct 26 '22 at 14:56
  • Well that's the problem. As the api having multiple instances connected to the db, we can't make this serial coz it doesn't apply in this case. It is the typical case of counter in a distributed environment but I mostly wanted to let the db handle the work. Also given it's just a counter the operations on fetching and updating will be quite fast so even if it's a bottleneck point, I'm accepting it as it is. – ALex Oct 27 '22 at 07:18

1 Answers1

0

This is how I've achieved what i needed.

Long story short, i've used a sql function and I called it inside repository. I didn't need the trigger anymore.

https://stackoverflow.com/a/74208072/3018285

ALex
  • 673
  • 1
  • 6
  • 19