pagy icon indicating copy to clipboard operation
pagy copied to clipboard

Feature: async_count

Open ddnexus opened this issue 3 months ago • 17 comments

Implementing the async_count feature/option, tests and docs.

ddnexus avatar Sep 28 '25 16:09 ddnexus

For test, it would probably be a good idea to use AR and the /pagy/test/files/db/test.sqlite3 as is for this feature. BTW, the tests are currently just failing the coverage, so we just need to add the cases to cover :async_cont option.

ddnexus avatar Sep 29 '25 06:09 ddnexus

just wanted to ping back that I haven't forgotten about this. just have a lot on my plate rn, but will look into this shortly

julianrubisch avatar Oct 02 '25 16:10 julianrubisch

Added a test. I'm not totally sure how to test the count_over scenario. Probably with Event and group_by_period, but I haven't found an ActiveRecord example of that in your codebase. Could you advise?

julianrubisch avatar Oct 02 '25 17:10 julianrubisch

Sorry for the late answer... The test of the :count_over is done here:

https://github.com/ddnexus/pagy/blob/e278b7020705f9e44a0f10c33eb56bdaf30db4ef/test/pagy/toolbox/paginators/offset_test.rb#L46-L58

As you see we mock it. 🤷🏻‍♂️ I have no idea about how to unify that.

ddnexus avatar Oct 14 '25 13:10 ddnexus

@ddnexus Are you sure this is relevant?

Maybe I don't understand async queries correctly, but to my understanding, async queries work like that:

count = async_count(:all)
# => starts querying in a different thread

# proceed to other slow computation on the main thread
ruby_method1
ruby_method2

# wait until SQL query is done
count.value
# return value in the main thread

Which means that async_count(:all).value would behave just like a sync query, since .value is called right after.

We should delay the call to value to the very end, ideally when it's time to render the pagination view.

n-studio avatar Oct 15 '25 15:10 n-studio

@n-studio, the expert in async queries is @julianrubisch here...

https://www.rorvswild.com/blog/2025/optimize-pagination-speed-with-asynchronous-queries-in-ruby-on-rails

ddnexus avatar Oct 15 '25 17:10 ddnexus

We should delay the call to value to the very end, ideally when it's time to render the pagination view.

It makes sense to me, but... @julianrubisch ?

If we're going to do that, we need to:

  • get the count promise in the controller
  • use the pagy method in the view, passing it count: count_promise.value

At that point the count promise would have to be obtained and passed by the user, so pagy wouldn't have nothing to do with it.

In that case the theoretically possible integration with the get_count method would be just a complicated overhead and this draft would be closed.

ddnexus avatar Oct 16 '25 06:10 ddnexus

@ddnexus I haven't read how pagy works yet but would something like this work?

def paginate(context, collection, **options)
...
        options[:count] ||= collection.instance_of?(Array) ? collection.size : OffsetPaginator.get_count(collection, options)
...
    end

def get_count(collection, options)
...
                options[:async_count] ? collection.async_count(:all): collection.count(:all)
...
    end


# In view
def count
  options[:count].is_a?(ActiveRecord::Promise) ? options[:count].value : options[:count]
end

n-studio avatar Oct 16 '25 07:10 n-studio

I don't have the time for a detailed answer rn (sorry), and I'm NOT the expert 😅

all I can say is I integrated it into my approach from here and it delivered the expected results (0.0ms count query).

julianrubisch avatar Oct 16 '25 07:10 julianrubisch

I can spend more time digging into this and set up benchmarks etc., but I don't have that much time at the moment.

A look at the Rails source also helps: https://github.com/rails/rails/blob/529f933fc8b13114d308dd0752f76a9e293c8537/activerecord/lib/active_record/promise.rb#L20 https://github.com/rails/rails/blob/529f933fc8b13114d308dd0752f76a9e293c8537/activerecord/lib/active_record/future_result.rb#L123

=> .value is called asynchronously at any point and ONLY called synchronously if the main thread "surpassed" the spawned query executor thread. Rails put some nifty safeguard there.

julianrubisch avatar Oct 16 '25 08:10 julianrubisch

@n-studio the count integer is necessary to create an offset paginator instance.

If your observation about calling value immediately is right, then the only way to get the async advantage, would be starting the new async thread in the controller, and moving the instantiation in the view.

ddnexus avatar Oct 17 '25 05:10 ddnexus

.value is called asynchronously at any point and ONLY called synchronously if the main thread "surpassed" the spawned query executor thread. Rails put some nifty safeguard there.

@julianrubisch I read this comment in the value method definition:

"If the query wasn't completed yet, accessing value will block until the query completes."

The point made by @n-studio is that even if we are creating a new thread asynchronously, (async_count frees the main thread) we block it immediately by calling value without executing any useful code in between.

The very next line has the count set as an integer, so the query has been executed in the previous line.

It seems to me that the he fact that the AR log reports zero time for it doesn't mean that the main thread is doing anything else than waiting for the result.

Maybe we could log a couple of timestamps before and after the line and have an answer.

ddnexus avatar Oct 17 '25 06:10 ddnexus

@n-studio the count integer is necessary to create an offset paginator instance.

If your observation about calling value immediately is right, then the only way to get the async advantage, would be starting the new async thread in the controller, and moving the instantiation in the view.

@ddnexus I see... I don't think count is necessary for the offset, right? I'd suggest that we compute the count in another class, like Pagy::Count instead of Pagy::OffsetPaginator

Otherwise, it would be impossible to run the main query and the count query concurrently, which would be a performance improvement in virtually all use cases.

Would it be possible?

n-studio avatar Oct 17 '25 06:10 n-studio

@ddnexus I see... I don't think count is necessary for the offset, right?

Count is necessary for the Pagy::Offset instantiation.

ddnexus avatar Oct 17 '25 08:10 ddnexus

Maybe we could log a couple of timestamps before and after the line and have an answer.

@julianrubisch, please could you try that?

ddnexus avatar Nov 01 '25 08:11 ddnexus

done!

julianrubisch avatar Nov 07 '25 12:11 julianrubisch

@julianrubisch thank you.

Sorry, I didn't explain myself. I should have asked you to run it in an actual environment and report the behavior/timing, since I am not organized to do it myself. 😬

ddnexus avatar Nov 08 '25 01:11 ddnexus