fragmentary 0.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +113 -71
- data/fragmentary.gemspec +1 -2
- data/lib/fragmentary/config.rb +5 -1
- data/lib/fragmentary/fragment.rb +33 -6
- data/lib/fragmentary/fragments_helper.rb +19 -3
- data/lib/fragmentary/handler.rb +24 -0
- data/lib/fragmentary/{dispatcher.rb → jobs/dispatch_handlers_job.rb} +6 -6
- data/lib/fragmentary/jobs/send_requests_job.rb +26 -0
- data/lib/fragmentary/request_queue.rb +18 -11
- data/lib/fragmentary/serializers/handler_serializer.rb +24 -0
- data/lib/fragmentary/serializers/request_queue_serializer.rb +36 -0
- data/lib/fragmentary/user_session.rb +3 -1
- data/lib/fragmentary/version.rb +1 -1
- data/lib/fragmentary.rb +10 -0
- metadata +9 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61a94b394035bb4b3ec317316e46fa77ca925cc15a162a76d6401500e6636b5d
|
4
|
+
data.tar.gz: 8db2f91e9a9b23f3ccdafbc2f800695e653426a656887959f807c80eca10c901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc4f5da40872cfb5327af28cb81862e61ba201258bc8338fa6ad9871439dac0133af4c102c10b09cd0ed25acb883c53d428ba3632085ac4b67cb7f662ccf6c16
|
7
|
+
data.tar.gz: '0628b394849e14ed4fed5ebe18e7b59a4badfe83d16b63f24965e6e6a2a4d656aec57f627000d70286c79152d2d028fb231c0b7c89a2eabfffb455140f7404e6'
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
### 0.4.0
|
2
|
+
- Uses ActiveJob as the interface for asynchronous processes instead of using Delayed::Job directly.
|
3
|
+
- Updates Rails dependency to ~>6.0 as earlier versions lack support for the custom job serialization needed to use ActiveJob. Accordingly, Rails 5.x and earlier are no longer supported.
|
4
|
+
- Adds a convenience method RequestQueue.send_all for sending internal application requests.
|
5
|
+
- Inserts optional HTML comments containing time and version stamps into fragment content.
|
6
|
+
- Adds utility methods for interacting with the cache_store from the console.
|
7
|
+
- Updates documentation for new features and to reflect Rails use of cache versioning.
|
8
|
+
|
1
9
|
### 0.3.0
|
2
10
|
- Updates gem to support Rails 5.x (Rails 4.x and earlier are no longer supported due to Rails API changes).
|
3
11
|
- Adds support for multiple application instances, allowing pre-release application code to be staged for testing.
|
data/README.md
CHANGED
@@ -10,12 +10,16 @@ Fragmentary augments the fragment caching capabilities of Ruby on Rails to suppo
|
|
10
10
|
Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use.
|
11
11
|
|
12
12
|
## Background
|
13
|
-
In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record.
|
13
|
+
In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. In the view, the `cache` helper is used to specify the record and to define the content to be rendered within the fragment, e.g.:
|
14
14
|
```
|
15
15
|
<% cache product do %>
|
16
16
|
<%= render product %>
|
17
17
|
<% end %>
|
18
18
|
```
|
19
|
+
The content is stored in the cache using a _key_ value derived from the record's `id` attribute. The key identifies where the cached content is stored. In addition, a _version_ value derived from the record's `updated_at` attribute is stored along with the content [^1]. If any attributes of the record change, the `updated_at` attribute also changes; the next time a browser requests the content, a mismatch will exist between the calculated and stored values of the version, causing the cache to expire and the fragment to be re-rendered using the current data.
|
20
|
+
|
21
|
+
[^1]: Prior to Rails 5.2, the value of `updated_at` was incorporated into the cache key rather that the version. That behavior is still available by setting `config.active_record.cache_versioning = false` in `config/application.rb`.
|
22
|
+
|
19
23
|
For data models with a `has_one` or `has_many` association, nested (Russian Doll) caching is possible with the inner fragment representing the associated record. For example if a product has many games -
|
20
24
|
```
|
21
25
|
<% cache product do %>
|
@@ -35,14 +39,14 @@ end
|
|
35
39
|
|
36
40
|
Part of the beauty of this approach is the fact that any change in data requiring the cache to be refreshed is detected by inspecting just a single `updated_at` attribute of each record. However it is less effective at handling cases where the content of a fragment depends in more complex ways on multiple records from multiple models. The fact that automatic updating of nested fragments is limited to `belongs_to` associations is one aspect of this.
|
37
41
|
|
38
|
-
It is certainly possible to construct more complex keys from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
|
42
|
+
It is certainly possible to construct more complex keys and/or versions from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
|
39
43
|
|
40
|
-
A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built,
|
44
|
+
A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, though we are still left with the challenge of how to implement nesting in the case of more complex data associations.
|
41
45
|
|
42
46
|
## Fragmentary - General Approach
|
43
|
-
Fragmentary
|
47
|
+
In Fragmentary, view fragments are associated with records from an ActiveRecord model and corresponding database table that are _separate_ from your application data. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the version of a cache entry is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
|
44
48
|
|
45
|
-
To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that
|
49
|
+
To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that all of the cached content containing the change is refreshed.
|
46
50
|
|
47
51
|
Rather than use Rails' native `cache` helper directly, Fragmentary provides some new helper methods that hide some of the necessary internals involved in working with an explicit fragment model. Additional features include the ability to cache separate versions of fragments for different types of users and the ability to insert user-specific content after cached content is retrieved from the cache store. Fragmentary also supports automatic background updating of cached content within seconds of application data changing, avoiding the need for a user to visit a page in order to update the cache.
|
48
52
|
|
@@ -117,15 +121,30 @@ class ProductTemplate < Fragment
|
|
117
121
|
needs_record_id :type => 'Product'
|
118
122
|
end
|
119
123
|
```
|
120
|
-
Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record
|
124
|
+
Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record and the `record_id` attribute of a `ProductTemplate` record contains the `id` of that product.
|
125
|
+
|
126
|
+
If you need to define further subclasses of your initial subclass, you can declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
|
121
127
|
|
122
128
|
`set_record_type 'SomeModelName'`
|
123
129
|
|
124
|
-
We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics but are used in different contexts to represent different
|
130
|
+
We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics in common but are used in different contexts to represent content with different sub-class-specific dependencies on application data.
|
131
|
+
|
132
|
+
Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block to define methods that handle create, update and destroy events on your application data, typically to touch the fragment records affected by a change in the application data. For example, if product listings include the names of all the categories the products belong to, and those categories are represented by a separate ActiveRecord model, if the wording of a category name changes, you could handle that as follows:
|
133
|
+
```
|
134
|
+
class ProductTemplate < Fragment
|
135
|
+
needs_record_id :type => 'Product'
|
125
136
|
|
126
|
-
|
137
|
+
subscribe_to 'ProductCategory' do
|
138
|
+
def update_product_category_successful(product_category)
|
139
|
+
# touch ProductTemplate records whose record_id matches the ids of products within the specified product_category
|
140
|
+
ProductTemplate.where(:record_id => product_category.products.map(&:id)).each(&:touch)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
The effect of this will be to expire the product template fragment for every product contained within the affected category. `subscribe_to` takes one parameter, the name of the application data class being handled, while the names of the methods defined within the block follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
|
127
146
|
|
128
|
-
Within the body of each method you define within the
|
147
|
+
Within the body of each method that you define within the block, the convenience method `touch_fragments_for_record` can be used retrieve and touch the fragment records affected by the change in application data. This method takes an individual application data record or a record_id or an array of either. So the example above can be written instead as:
|
129
148
|
```
|
130
149
|
class ProductTemplate < Fragment
|
131
150
|
needs_record_id :type => 'Product'
|
@@ -137,9 +156,7 @@ class ProductTemplate < Fragment
|
|
137
156
|
end
|
138
157
|
end
|
139
158
|
```
|
140
|
-
|
141
|
-
|
142
|
-
When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining (so the methods we define within the block are actually defined on that subscriber object). However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
|
159
|
+
When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining. So the methods we define within the block are actually defined on that subscriber object. However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the associated fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
|
143
160
|
|
144
161
|
Note also that for fragment subclasses that declare `needs_record_id`, there is no need to define a `destroy_<model_name>_successful` method simply to remove a fragment whose `record_id` matches the `id` of a <model_name> application record that is destroyed, e.g. to destroy a `ProductTemplate` whose `record_id` matches the `id` of a destroyed `Product` object. Fragmentary handles this clean-up automatically. This is not to say, however, that 'destroy' handlers are never needed at all. The destruction of an application data record will often require other fragments to be touched.
|
145
162
|
|
@@ -158,7 +175,7 @@ A 'root' fragment is one that has no parent. In the template in which a root fra
|
|
158
175
|
|
159
176
|
#### Nested Fragments
|
160
177
|
|
161
|
-
The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment`
|
178
|
+
The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template's execution context (i.e. the receiver of the `cache_fragment` method) as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
|
162
179
|
```
|
163
180
|
<% cache_fragment :type => 'ProductTemplate', :record_id => @product.id do |fragment| %>
|
164
181
|
<% fragment.cache_child :type => 'StoresAvailable' do |child_fragment| %>
|
@@ -187,9 +204,11 @@ class StoresAvailable < Fragment
|
|
187
204
|
acts_as_list_fragment :members => :product_stores, :list_record => :product
|
188
205
|
end
|
189
206
|
```
|
207
|
+
The effect of this declaration is to ensure that whenever a new `ProductStore` is created, the `StoresAvailable` record for the corresponding product will be touched, so that the next time the list is rendered, the required new `AvailableStore` fragment will be created.
|
208
|
+
|
190
209
|
`acts_as_list_fragment` takes two named arguments:
|
191
|
-
* `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list.
|
192
|
-
* `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the record_id (or the record itself; either will work) associated with the list fragment from a given membership association.
|
210
|
+
* `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type is what triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. Note that the value of the `members` argument should match the record type of the list item fragments (i.e. the type provided to `needs_record_id` in the definition of the list item fragment class). So in the example, the `AvailableStore` class would be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implicit redundancy here that could be problematic; some adjustment may be made in the future).
|
211
|
+
* `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the `record_id` (or the record itself; either will work) associated with the list fragment from a given membership association. So, in the example, the membership association is a `ProductStore` record, say `product_store`, and `:product` is the method that would be called on `product_store` to produce the product whose `id` is the `record_id` of the `StoresAvailable` list fragment that needs to be touched when a new list item is created (`:product_id` would also work). Sometimes, however, a simple method like this is insufficient and a `Proc` may be used instead. The newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
|
193
212
|
|
194
213
|
Note that in the example, the specified `list_record` method, `:product`, returns a single record for a given `product_store` membership association. However this isn't necessarily always the case. The method or `Proc` may also return an array of records or record_ids.
|
195
214
|
|
@@ -219,7 +238,7 @@ then creating an initial product_store association won't add the initial item si
|
|
219
238
|
<% end %>
|
220
239
|
```
|
221
240
|
|
222
|
-
It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example this would look like the following.
|
241
|
+
It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example, this would look like the following.
|
223
242
|
```
|
224
243
|
class StoresAvailable < Fragment
|
225
244
|
subscribe_to 'ProductStore' do
|
@@ -230,13 +249,13 @@ class StoresAvailable < Fragment
|
|
230
249
|
end
|
231
250
|
```
|
232
251
|
|
233
|
-
In fact there can be cases where it is
|
252
|
+
In fact there can be cases where it is necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
|
234
253
|
|
235
254
|
### Accessing Fragments by Arbitrary Application Attributes
|
236
255
|
|
237
|
-
The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the fragment type, the `parent_id` if the fragment is nested
|
256
|
+
The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the `record_id`, the fragment type, and the `parent_id` if the fragment is nested.
|
238
257
|
|
239
|
-
Of course it is not always necessary to have a `record_id`, for example in
|
258
|
+
Of course, it is not always necessary to have a `record_id`, for example in a fragment representing a list of items on an index page. On the other hand, there are also cases where something other than a `record_id` is needed in order to uniquely identify a fragment. Suppose, for example, you have a fragment that renders a set of books published in a certain year, a set of restaurants in a certain postcode, or a set of competitors in a sporting event within a particular age-group category. In cases like these we need to be able to uniquely identify the fragment again by its type and sometimes its `parent_id` (if it is nested), but also by some other parameter, which we refer to as a 'key' (the terminology may be a little unfortunate; this is not the same thing as a _cache key_).
|
240
259
|
|
241
260
|
To facilitate this, the fragment model includes a `key` attribute that can be customized on a per-class basis using the `needs_key` method. For example,
|
242
261
|
```
|
@@ -253,7 +272,7 @@ With this definition we can define the cached content like this:
|
|
253
272
|
<% end %>
|
254
273
|
<% end %>
|
255
274
|
```
|
256
|
-
Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could
|
275
|
+
Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` (`year` in the example above) can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could in this example write `fragment.year_of_publication`.
|
257
276
|
|
258
277
|
### User Specific Content
|
259
278
|
|
@@ -489,9 +508,9 @@ When application data that a cached fragment depends upon changes, i.e. as a res
|
|
489
508
|
|
490
509
|
As part of this process, new fragment records may be created as *children* of an existing fragment if the existing fragment contains newly added list items. A new *root* fragment, on the other hand, will be created for any root fragment class defined with `needs_record_id` if an application data record of the associated `record_type` is created, as soon as a browser request is made for the new content (often a new page) containing that fragment.
|
491
510
|
|
492
|
-
Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur.
|
511
|
+
Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur. These 'internal' requests are sent asynchronously so that they do not delay the response to the external POST, PATCH or DELETE request from the user's browser that brought about the change in application data.
|
493
512
|
|
494
|
-
Creating
|
513
|
+
Creating internal requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (If required, the request can be suppressed by passing `:no_request => true` to `touch`). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
|
495
514
|
```
|
496
515
|
class ProductTemplate < Fragment
|
497
516
|
needs_record_id :type => 'Product'
|
@@ -523,15 +542,15 @@ So in this example, any time a new `Product` record is created, Fragmentary will
|
|
523
542
|
|
524
543
|
#### Request Queues
|
525
544
|
|
526
|
-
A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition,
|
545
|
+
A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, given that different versions of some cached fragments may exist for different user types, we may need to send each internal request multiple times in the context of several different user sessions (representing different user types), to ensure that all affected versions get refreshed.
|
527
546
|
|
528
|
-
To achieve this, during handling of the initial external request
|
547
|
+
To achieve this, during the handling of the initial external request that causes the application data to change, the internal requests created as a result are represented by instances of the class `Fragmentary::Request` and are initially stored in a number of 'queues' represented by instances of the class `Fragmentary::RequestQueue`. Separate queues exist for each of the different user types supported by the fragments to be rendered, and individual requests to a given endpoint are automatically placed in multiple queues as needed. However, responsibility for actually sending the requests stored in the queues is offloaded to an asynchronous process using Rails' `ActiveJob` interface (as detailed below under [Sending Queued Requests](https://github.com/MarkMT/fragmentary#sending-queued-requests)).
|
529
548
|
|
530
|
-
|
549
|
+
`Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports. So, for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`.
|
531
550
|
|
532
|
-
|
551
|
+
The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user). Configuration of the `user_types` method is discussed in the next section.
|
533
552
|
|
534
|
-
The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example
|
553
|
+
The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example,`ProductTemplate.request_queues["signed_in"]` and `StoreTemplate.request_queues["signed_in"]` both represent the same object.
|
535
554
|
|
536
555
|
#### Configuring Internal Request Users
|
537
556
|
|
@@ -569,16 +588,34 @@ The value of the `:session_users` option (you can also use `:user_types` or just
|
|
569
588
|
|
570
589
|
#### Sending Queued Requests
|
571
590
|
|
572
|
-
|
591
|
+
Internal requests stored in the application's request queues are typically sent using an `after_action` filter in your `ApplicationController`, e.g. for create, update and destroy actions:
|
592
|
+
```
|
593
|
+
class ApplicationController
|
594
|
+
after_action :send_queued_requests, :only => [:create, :update, :destroy]
|
595
|
+
|
596
|
+
def send_queued_requests
|
597
|
+
Fragmentary::RequestQueue.send_all(:between => 10.seconds)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
```
|
601
|
+
The optional named argument `between` represents the interval between individual requests in each queue being sent and allows you to throttle the request rate. If it is present (including `:between => 0.seconds`), the requests will be sent asynchronously using ActiveJob. If omitted, requests will be sent immediately (i.e before the response to the original external request is sent to the user's browser).
|
573
602
|
|
574
|
-
|
603
|
+
You can customize further by sending requests from queues individually. A class method `Fragmentary::RequestQueue.all` returns an array of all request queues that the application uses. The requests stored within an individual queue can be sent by calling the instance method `RequestQueue#send`.
|
575
604
|
```
|
576
605
|
def send_queued_requests
|
577
606
|
delay = 0.seconds
|
578
607
|
Fragmentary::RequestQueue.all.each{|q| q.send(:delay => delay += 10.seconds)}
|
579
608
|
end
|
580
609
|
```
|
581
|
-
|
610
|
+
`send` takes two optional named arguments, `delay` and `between`. `delay` represents the delay before the queue begins sending requests and `between` again represents the interval between individual requests. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. If either are present (including `:delay => 0`), the requests will be sent asynchronously using ActiveJob. If omitted, all requests held in the queue are sent immediately.
|
611
|
+
|
612
|
+
When requests are sent using either `send` or `send_all`, a `Fragmentary::UserSession` object is instantiated representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` automatically signs in to the application using the credentials configured for the particular `user_type`.
|
613
|
+
|
614
|
+
ActiveJob supports a number of adapters for executing tasks asynchronously. You should configure according to your own needs. For example, to use Fragmentary with the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem, you would add the following to your `config/application.rb` file:
|
615
|
+
```
|
616
|
+
config.active_job.queue_adapter = :delayed_job
|
617
|
+
```
|
618
|
+
and add the 'delayed_job_active_record' gem to your Gemfile. Consult the [ActiveJob documentation](https://guides.rubyonrails.org/active_job_basics.html#backends) for more information as well as the documentation for your chosen adapter on how to start background task processes when you deploy your application.
|
582
619
|
|
583
620
|
#### Queuing Requests Explicitly
|
584
621
|
|
@@ -628,31 +665,20 @@ class AvailableStore < Fragment
|
|
628
665
|
end
|
629
666
|
end
|
630
667
|
```
|
631
|
-
While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process
|
668
|
+
While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process. We again do this using ActiveJob in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
|
632
669
|
|
633
|
-
To facilitate this we use the `Fragmentary::Dispatcher` class. A dispatcher object is instantiated with an array containing all existing handlers, retrieved using the class method `Fragmentary::Handler.all`.
|
634
670
|
```
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
671
|
+
def handle_asynchronous_tasks
|
672
|
+
send_queued_requests # as defined earlier
|
673
|
+
Fragmentary::DispatchHandlersJob.perform_later(Fragmentary::Handler.all)
|
674
|
+
Fragmentary::Handler.clear
|
675
|
+
end
|
640
676
|
```
|
641
|
-
|
677
|
+
Here `Fragmentary::DispatchHandlersJob` is a subclass of `ActiveJob::Base`, which means if you wish you can specify additional options using the `set` method, e.g.
|
642
678
|
```
|
643
|
-
|
644
|
-
|
645
|
-
after_filter :handle_asynchronous_tasks
|
646
|
-
|
647
|
-
def handle_asynchronous_tasks
|
648
|
-
send_queued_requests # as defined earlier
|
649
|
-
Delayed::Job.enqueue(Fragmentary::Dispatcher.new(Fragmentary::Handler.all))
|
650
|
-
Fragmentary::Handler.clear
|
651
|
-
end
|
652
|
-
|
653
|
-
end
|
679
|
+
Fragmentary::DispatchHandlersJob.set(:wait => 1.minute, :queue => 'low_priority').perform_later(Fragmentary::Handler.all)
|
654
680
|
```
|
655
|
-
|
681
|
+
Also note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. `DispatchHandlersJob` also takes care of sending these additional requests.
|
656
682
|
|
657
683
|
#### Updating Lists Asynchronously
|
658
684
|
|
@@ -790,7 +816,6 @@ Fragmentary.setup do |config|
|
|
790
816
|
...
|
791
817
|
config.application_root_url_column = 'app_root'
|
792
818
|
end
|
793
|
-
|
794
819
|
```
|
795
820
|
|
796
821
|
Once this column has been added to the `fragments` table, any fragment records subsequently created by calls to `cache_fragment` or `cache_child` will have the column populated automatically based on the particular application instance in which the code is executed, and the content stored in the cache for each fragment record will be that generated by the corresponding instance. As long as any links rendered within the content are generated using Rails' path or url helpers, the cached content will be rendered correctly both when initially created and when retrieved subsequently.
|
@@ -803,11 +828,9 @@ The fact that two versions of content exist in the cache for each fragment means
|
|
803
828
|
|
804
829
|
(Note: for better or worse, we've stuck with the term 'internal request' to mean any request delivered to the application programmatically in order to refresh cached content. This includes requests created by one application instance that are intended to be processed by another instance.)
|
805
830
|
|
806
|
-
Our approach to sending requests between application instances relies on requests
|
831
|
+
Our approach to sending requests between application instances relies on processing requests asynchronously and directing the tasks used to do this to specific task queues associated with the application instance they are intended to be processed by. Each application instance has an associated asynchronous task process (in our case using Delayed::Job). By configuring that process to run tasks from just the queue(s) designated for it, we ensure that any application instance can direct requests to any other. As long as queued tasks are stored in a database table (as they are, for example, using Delayed::Job) and provided that each instance of the application has access to the same database, this approach achieves our objective. The way we accomplish this in practice is outlined below.
|
807
832
|
|
808
|
-
|
809
|
-
|
810
|
-
To configure Fragmentary to automatically refresh cached content for multiple instances, first set `remote_urls` in `Fragmentary.config`. This is an array of root URLs for all _other_ instances of the application that requests should be sent to. For example, in order to allow requests to be sent from the production instance to the pre-release instance, in initializers/fragmentary.rb in the production code we would add the following configuration:
|
833
|
+
First, to configure Fragmentary to automatically refresh cached content for multiple instances, set `remote_urls` in `Fragmentary.config`. This is an array of root URLs for all _other_ instances of the application that requests should be sent to. For example, in order to allow requests to be sent from the production instance to the pre-release instance, in initializers/fragmentary.rb in the production code we would add the following configuration:
|
811
834
|
|
812
835
|
```
|
813
836
|
Fragmentary.setup do |config|
|
@@ -818,38 +841,57 @@ end
|
|
818
841
|
|
819
842
|
Our current approach to application deployment is to maintain different branches in our source repository for each application instance. This allows us to keep custom configurations like `config.remote_urls` above for each instance on their own branches. So in contrast to the case above, to allow requests to be sent from the pre-release instance to the production instance, in initializers/fragmentary.rb on the pre-release branch the `remote_urls` array would be set to `['http://myapp.com/']` .
|
820
843
|
|
821
|
-
As an alternative, it
|
822
|
-
|
823
|
-
By specifying `remote_urls` as described above, for each internal application request Fragmentary will automatically create tasks to process the request not only on the application instance where it was created but also on all other instances corresponding to the elements in `remote_urls`. Fragmentary sends these tasks to [Delayed::Job](https://github.com/collectiveidea/delayed_job) queues that have names formed from the domain name and path of both the root URL of the current instance and the values in `remote_urls`. So in the example, a request to '/products/123', created say by editing a product name on the production website, would be sent to [Delayed::Job](https://github.com/collectiveidea/delayed_job) queues with names 'myapp.com' and 'myapp.com/prerelease/'.
|
824
|
-
|
825
|
-
The final step needed to allow internal requests to be processed by the instance they are intended for is to configure the [Delayed::Job](https://github.com/collectiveidea/delayed_job) process associated with each instance to process tasks from the correct queue.
|
844
|
+
As an alternative, it would also be possible to create a separate environment for the pre-release deployment in your config/environments directory and thus maintain the configuration for all application instances in a single repository branch. We have not investigated this approach.
|
826
845
|
|
827
|
-
|
846
|
+
By specifying `remote_urls` as described above, for each internal application request Fragmentary will automatically create tasks to process the request not only on the application instance where it was created but also on all other instances corresponding to the elements in `remote_urls`. Fragmentary sends these tasks to named queues whose names are formed automatically from the domain name and path of both the root URL of the current instance and the values in `remote_urls`. So in the example, a request to '/products/123', created say by editing a product name on the production website, would be sent to queues with names 'myapp.com/' and 'myapp.com/prerelease/'.
|
828
847
|
|
848
|
+
The final step is to configure the asynchronous processes associated with each application instance to execute tasks from the correct queue. In our case, we use [Delayed::Job](https://github.com/collectiveidea/delayed_job) to handle asynchronous tasks and Capistrano 2 for deployment. We set `delayed_job_args` in `config/deploy.rb` to configure the queue names as follows (see documentation [here](https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/recipes.rb)):
|
829
849
|
```
|
830
850
|
queue_prefix = "myapp.com/prerelease/"
|
831
|
-
set :delayed_job_args, "--queue=#{queue_prefix}
|
851
|
+
set :delayed_job_args, "--queue=#{queue_prefix}"
|
832
852
|
after "deploy:stop", "delayed_job:stop"
|
833
853
|
after "deploy:start", "delayed_job:start"
|
834
854
|
after "deploy:restart", "delayed_job:restart"
|
835
855
|
```
|
856
|
+
Again, this configuration will be different for each instance, and so in our case each one is stored in different source repository branches.
|
836
857
|
|
837
|
-
|
838
|
-
|
839
|
-
Note that if you configure [Delayed::Job](https://github.com/collectiveidea/delayed_job) to work only from specific queues, you'll need to make sure that _any_ asynchronous tasks created by your application are submitted to one of those queues.
|
858
|
+
Note that if you configure [Delayed::Job](https://github.com/collectiveidea/delayed_job) (or other backend) to work only from specific queues, you'll need to make sure that _any_ asynchronous tasks created by your application are submitted to one of those queues.
|
840
859
|
|
841
860
|
## Dependencies
|
842
861
|
|
843
|
-
-
|
844
|
-
|
862
|
+
- Fragmentary is currently being used in production with Rails 6.0. Versions >=0.4 do not work with earlier versions of Rails for a couple of reasons:
|
863
|
+
- The version of ActiveJob in Rails 5.x does not support the custom serialization needed to enqueue asynchronous tasks for sending internal requests. A repository branch 'delayed_job' is available that uses the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem directly (i.e. without ActiveJob) and this works well with Rails 5.x. However, this branch is no longer maintained.
|
864
|
+
- The API for the Rails `ActionDispatch::Integration::Session` class, which is used to handle internal application requests, was changed in Rails 5.0. Consequently the current version of Fragmentary is incompatible with Rails 4.x and earlier. We do have a 'Rails.4.2' branch in the repository that uses the older API. However, this branch is no longer maintained.
|
845
865
|
|
846
|
-
##
|
866
|
+
## Timestamps
|
847
867
|
|
848
|
-
|
868
|
+
Fragmentary allows HTML comments to be optionally inserted into cached fragments identifying the time at which the fragment was last cached. Simply add `config.insert_timestamps = true` to the configuration block in your `config/initializers/fragmentary.rb` file.
|
869
|
+
|
870
|
+
It is also possible to insert comments identifying a version name for the application code in use at the time the fragment was cached and the time at which that version was deployed. This is accomplished by dynamically adding values for the configuration settings `Fragmentary.config.release_name` and `Fragmentary.config.deployed_at` to your `config/initializers/fragmentary.rb` file at the time you deploy. ie. within your deployment script you need to arrange to append something like the following to that file.
|
871
|
+
```
|
872
|
+
Fragmentary.config.release_name = release_name
|
873
|
+
Fragmentary.config.deployed_at = deployed_at
|
874
|
+
```
|
875
|
+
It is up to you how you define/determine these values, but `deployed_at` would typically represent the value of `Time.now.utc` at the time the deployment script is executed. If these two configuration values are set, and `Fragmentary.config.insert_timestamps` is set to true, Fragmentary will automatically insert comments along the lines of the following:
|
876
|
+
```
|
877
|
+
<!-- ProductDetails 2382 cached by Fragmentary version 0.4 at 2023-09-22 09:28:02 UTC -->
|
878
|
+
<!-- Cached using application release 20230919233149 deployed at 2023-09-19 23:33:16 UTC -->
|
879
|
+
... Cached content appears here ...
|
880
|
+
<!-- ProductDetails 2382 ends -->
|
881
|
+
```
|
882
|
+
|
883
|
+
## Utility Methods
|
849
884
|
|
850
|
-
|
885
|
+
The `Fragment` class and its subclasses provide a number of utility instance methods that are typically useful for interacting with fragments from the console.
|
886
|
+
- `touch_or_destroy` - recursively touches the fragment record and all decendant fragments for which cache entries exist. Destroys any fragment records in the tree for which no cache entry exists.
|
887
|
+
- `cache_exist?` - returns a boolean indicating whether fragment has content stored in the cache.
|
888
|
+
- `content` - retrieves the fragment's cached content
|
889
|
+
- `delete_cache` - removes the fragment's content from the cache.
|
890
|
+
- `delete_cache_tree` - recursively removes from the cache all content for the fragment and its descendants.
|
851
891
|
|
852
|
-
|
892
|
+
## Contributing
|
893
|
+
|
894
|
+
Bug reports and usage questions are welcome at https://github.com/MarkMT/fragmentary.
|
853
895
|
|
854
896
|
## License
|
855
897
|
|
data/fragmentary.gemspec
CHANGED
@@ -22,8 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.bindir = "exe"
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
|
-
spec.add_runtime_dependency "rails", "~>
|
26
|
-
spec.add_runtime_dependency "delayed_job_active_record", "~> 4.1"
|
25
|
+
spec.add_runtime_dependency "rails", "~> 6.0"
|
27
26
|
spec.add_runtime_dependency "wisper-activerecord", "~> 1.0"
|
28
27
|
spec.add_runtime_dependency "http", "~> 3.0.0"
|
29
28
|
spec.add_runtime_dependency "nokogiri"
|
data/lib/fragmentary/config.rb
CHANGED
@@ -3,13 +3,17 @@ module Fragmentary
|
|
3
3
|
class Config
|
4
4
|
include Singleton
|
5
5
|
attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path, :sign_out_path,
|
6
|
-
:users, :default_user_type_mapping, :session_users, :application_root_url_column,
|
6
|
+
:users, :default_user_type_mapping, :session_users, :application_root_url_column,
|
7
|
+
:remote_urls, :insert_timestamps, :deployed_at, :release_name
|
7
8
|
|
8
9
|
def initialize
|
9
10
|
# default
|
10
11
|
@current_user_method = :current_user
|
11
12
|
@application_root_url_column = :application_root_url
|
12
13
|
@remote_urls = []
|
14
|
+
@insert_timestamps = false
|
15
|
+
@deployed_at = nil
|
16
|
+
@release_name = nil
|
13
17
|
end
|
14
18
|
|
15
19
|
def session_users=(session_users)
|
data/lib/fragmentary/fragment.rb
CHANGED
@@ -146,7 +146,7 @@ module Fragmentary
|
|
146
146
|
def request_queues
|
147
147
|
super # ensure that @@request_queues has been defined
|
148
148
|
@request_queues ||= begin
|
149
|
-
app_root_url =
|
149
|
+
app_root_url = Fragmentary.application_root_url
|
150
150
|
remote_urls = Fragmentary.config.remote_urls
|
151
151
|
user_types.each_with_object( Hash.new {|hsh0, url| hsh0[url] = {}} ) do |user_type, hsh|
|
152
152
|
# Internal request queues
|
@@ -494,12 +494,15 @@ module Fragmentary
|
|
494
494
|
end
|
495
495
|
|
496
496
|
def touch(*args, no_request: false)
|
497
|
+
@no_request = no_request # stored for use in #touch_parent via the after_commit callback
|
497
498
|
request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request && !no_request
|
498
499
|
super(*args)
|
499
500
|
end
|
500
501
|
|
502
|
+
# delete the associated cache content before destroying the fragment
|
501
503
|
def destroy(options = {})
|
502
504
|
options.delete(:delete_matches) ? delete_matched_cache : delete_cache
|
505
|
+
@no_request = options.delete(:no_request) # stored for use in #touch_parent via the after_commit callback
|
503
506
|
super()
|
504
507
|
end
|
505
508
|
|
@@ -508,9 +511,17 @@ module Fragmentary
|
|
508
511
|
end
|
509
512
|
|
510
513
|
def delete_cache
|
511
|
-
cache_store.delete(
|
514
|
+
cache_store.delete(fragment_key)
|
512
515
|
end
|
513
516
|
|
517
|
+
# Recursively delete the cache entry for this fragment and all of its children
|
518
|
+
# Does NOT destroy the fragment or its children
|
519
|
+
def delete_cache_tree
|
520
|
+
children.each(&:delete_cache_tree)
|
521
|
+
delete_cache if cache_exist?
|
522
|
+
end
|
523
|
+
|
524
|
+
# Recursively touch the fragment and all of its children
|
514
525
|
def touch_tree(no_request: false)
|
515
526
|
children.each{|child| child.touch_tree(:no_request => no_request)}
|
516
527
|
# If there are children, we'll have already touched this fragment in the process of touching them.
|
@@ -520,19 +531,34 @@ module Fragmentary
|
|
520
531
|
# Touch this fragment and all descendants that have entries in the cache. Destroy any that
|
521
532
|
# don't have cache entries.
|
522
533
|
def touch_or_destroy
|
523
|
-
puts " touch_or_destroy #{self.class.name} #{id}"
|
524
534
|
if cache_exist?
|
525
535
|
children.each(&:touch_or_destroy)
|
526
536
|
# if there are children, this will be touched automatically once they are.
|
527
537
|
touch(:no_request => true) unless children.any?
|
528
538
|
else
|
529
|
-
destroy # will also destroy all children because of :dependent => :destroy
|
539
|
+
destroy(:no_request => true) # will also destroy all children because of :dependent => :destroy
|
530
540
|
end
|
531
541
|
end
|
532
542
|
|
533
543
|
def cache_exist?
|
534
544
|
# expand_cache_key calls cache_key and prepends "views/"
|
535
|
-
cache_store.exist?(
|
545
|
+
cache_store.exist?(fragment_key)
|
546
|
+
end
|
547
|
+
|
548
|
+
|
549
|
+
# Typically used along with #cache_exist? when testing from the console.
|
550
|
+
# Note that both methods will only return correct results for fragments associated with the application_root_url
|
551
|
+
# (either root or children) corresponding to the particular console session in use. i.e. you can't see into the
|
552
|
+
# production cache from a prerelease console session and vice versa.
|
553
|
+
def content
|
554
|
+
cache_store.read(fragment_key)
|
555
|
+
end
|
556
|
+
|
557
|
+
# This emulates the result of passing the fragment object to AbstractController::Caching::Fragments#combined_fragment_cache_key
|
558
|
+
# when the cache helper method invokes controller.read_fragment from the view. The result can be passed to ActiveSupport::Cache methods
|
559
|
+
# #read, #write, #fetch, #delete, and #exist?
|
560
|
+
def fragment_key
|
561
|
+
['views', self]
|
536
562
|
end
|
537
563
|
|
538
564
|
# Request-related methods...
|
@@ -570,7 +596,8 @@ module Fragmentary
|
|
570
596
|
|
571
597
|
private
|
572
598
|
def touch_parent
|
573
|
-
parent.try
|
599
|
+
parent.try(:touch, {:no_request => @no_request}) unless previous_changes["memo"]
|
600
|
+
@no_request = false
|
574
601
|
end
|
575
602
|
|
576
603
|
module NeedsRecordId
|
@@ -3,7 +3,7 @@ module Fragmentary
|
|
3
3
|
module FragmentsHelper
|
4
4
|
|
5
5
|
def cache_fragment(options, &block)
|
6
|
-
options.reverse_merge!(Fragmentary.config.application_root_url_column =>
|
6
|
+
options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
|
7
7
|
CacheBuilder.new(self).cache_fragment(options, &block)
|
8
8
|
end
|
9
9
|
|
@@ -11,7 +11,7 @@ module Fragmentary
|
|
11
11
|
# the template option is deprecated but avoids breaking prior usage
|
12
12
|
template = options.delete(:template) || self
|
13
13
|
options.reverse_merge!(:user => Template.new(template).current_user)
|
14
|
-
options.reverse_merge!(Fragmentary.config.application_root_url_column =>
|
14
|
+
options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
|
15
15
|
CacheBuilder.new(template, Fragmentary::Fragment.base_class.existing(options))
|
16
16
|
end
|
17
17
|
|
@@ -36,7 +36,16 @@ module Fragmentary
|
|
36
36
|
builder = CacheBuilder.new(@template, next_fragment)
|
37
37
|
unless no_cache
|
38
38
|
@template.cache next_fragment, :skip_digest => true do
|
39
|
-
|
39
|
+
if Fragmentary.config.insert_timestamps
|
40
|
+
@template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} cached by Fragmentary version #{VERSION} at #{Time.now.utc} -->")
|
41
|
+
if deployed_at && release_name
|
42
|
+
@template.safe_concat("<!-- Cached using application release #{release_name} deployed at #{deployed_at} -->")
|
43
|
+
end
|
44
|
+
yield(builder)
|
45
|
+
@template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} ends -->")
|
46
|
+
else
|
47
|
+
yield(builder)
|
48
|
+
end
|
40
49
|
end
|
41
50
|
else
|
42
51
|
yield(builder)
|
@@ -52,6 +61,13 @@ module Fragmentary
|
|
52
61
|
@fragment.send(method, *args)
|
53
62
|
end
|
54
63
|
|
64
|
+
def deployed_at
|
65
|
+
@deployed_at ||= Fragmentary.config.deployed_at
|
66
|
+
end
|
67
|
+
|
68
|
+
def release_name
|
69
|
+
@release_name ||= Fragmentary.config.release_name
|
70
|
+
end
|
55
71
|
end
|
56
72
|
|
57
73
|
end
|
data/lib/fragmentary/handler.rb
CHANGED
@@ -1,4 +1,26 @@
|
|
1
1
|
module Fragmentary
|
2
|
+
|
3
|
+
class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
|
4
|
+
|
5
|
+
def serialize?(arg)
|
6
|
+
arg.is_a? Fragmentary::Handler
|
7
|
+
end
|
8
|
+
|
9
|
+
def serialize(handler)
|
10
|
+
super(
|
11
|
+
{
|
12
|
+
:class_name => handler.class.name,
|
13
|
+
:args => handler.args
|
14
|
+
}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(hsh)
|
19
|
+
hsh[:class_name].constantize.new(hsh[:args])
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
2
24
|
class Handler
|
3
25
|
def self.all
|
4
26
|
@@all
|
@@ -14,6 +36,8 @@ module Fragmentary
|
|
14
36
|
handler
|
15
37
|
end
|
16
38
|
|
39
|
+
attr_reader :args
|
40
|
+
|
17
41
|
def initialize(**args)
|
18
42
|
@args = args
|
19
43
|
end
|
@@ -1,12 +1,11 @@
|
|
1
|
+
require 'fragmentary/serializers/handler_serializer'
|
2
|
+
|
1
3
|
module Fragmentary
|
2
4
|
|
3
|
-
class
|
4
|
-
def initialize(tasks)
|
5
|
-
@tasks = tasks
|
6
|
-
end
|
5
|
+
class DispatchHandlersJob < ActiveJob::Base
|
7
6
|
|
8
|
-
def perform
|
9
|
-
|
7
|
+
def perform(tasks)
|
8
|
+
tasks.each do |task|
|
10
9
|
Rails.logger.info "***** Dispatching task for handler class #{task.class.name}"
|
11
10
|
task.call
|
12
11
|
end
|
@@ -14,6 +13,7 @@ module Fragmentary
|
|
14
13
|
queue.start
|
15
14
|
end
|
16
15
|
end
|
16
|
+
|
17
17
|
end
|
18
18
|
|
19
19
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'fragmentary/serializers/request_queue_serializer'
|
2
|
+
|
3
|
+
module Fragmentary
|
4
|
+
|
5
|
+
class SendRequestsJob < ActiveJob::Base
|
6
|
+
|
7
|
+
after_perform :schedule_next
|
8
|
+
|
9
|
+
def perform(queue, delay: nil, between: nil, queue_suffix: '', priority: 0)
|
10
|
+
@queue = queue
|
11
|
+
@delay = delay
|
12
|
+
@between = between
|
13
|
+
@queue_suffix = queue_suffix
|
14
|
+
@priority = priority
|
15
|
+
@between ? @queue.send_next_request : @queue.send_all_requests
|
16
|
+
end
|
17
|
+
|
18
|
+
def schedule_next
|
19
|
+
if @queue.size > 0
|
20
|
+
self.enqueue(:wait => @between, :queue => @queue.target.queue_name + @queue_suffix, :priority => @priority)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -6,6 +6,18 @@ module Fragmentary
|
|
6
6
|
@@all ||= []
|
7
7
|
end
|
8
8
|
|
9
|
+
def self.send_all(between: nil)
|
10
|
+
unless between
|
11
|
+
all.each{|q| q.start}
|
12
|
+
else
|
13
|
+
unless between.is_a? ActiveSupport::Duration
|
14
|
+
raise TypeError, "Fragmentary::RequestQueue.send_all requires the keyword argument :between to be of class ActiveSupport::Duration. The value provided is of class #{between.class.name}."
|
15
|
+
end
|
16
|
+
delay = 0.seconds
|
17
|
+
all.each{|q| q.start(:delay => delay += between)}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
9
21
|
attr_reader :requests, :user_type, :host_root_url
|
10
22
|
|
11
23
|
def initialize(user_type, host_root_url)
|
@@ -72,9 +84,10 @@ module Fragmentary
|
|
72
84
|
def queue_name
|
73
85
|
@url.gsub(%r{https?://}, '')
|
74
86
|
end
|
87
|
+
|
75
88
|
end
|
76
89
|
|
77
|
-
attr_reader :queue
|
90
|
+
attr_reader :queue, :target, :delay, :between, :queue_suffix, :priority
|
78
91
|
|
79
92
|
def initialize(queue)
|
80
93
|
@queue = queue
|
@@ -90,9 +103,9 @@ module Fragmentary
|
|
90
103
|
end
|
91
104
|
|
92
105
|
# Send all requests, either directly or by schedule
|
93
|
-
def start(delay: nil, between: nil)
|
106
|
+
def start(delay: nil, between: nil, queue_suffix: '', priority: 0)
|
94
107
|
Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
|
95
|
-
@delay = delay; @between = between
|
108
|
+
@delay = delay; @between = between; @queue_suffix = queue_suffix; @priority = priority
|
96
109
|
if @delay or @between
|
97
110
|
schedule_requests(@delay)
|
98
111
|
# sending requests by schedule makes a copy of the sender and queue objects for
|
@@ -115,10 +128,6 @@ module Fragmentary
|
|
115
128
|
end
|
116
129
|
end
|
117
130
|
|
118
|
-
def success
|
119
|
-
schedule_requests(@between) if queue.size > 0
|
120
|
-
end
|
121
|
-
|
122
131
|
private
|
123
132
|
|
124
133
|
def send_all_requests
|
@@ -130,10 +139,8 @@ module Fragmentary
|
|
130
139
|
def schedule_requests(delay=0.seconds)
|
131
140
|
if queue.size > 0
|
132
141
|
clear_session
|
133
|
-
|
134
|
-
|
135
|
-
Delayed::Job.enqueue self, :run_at => delay.from_now, :queue => @target.queue_name
|
136
|
-
end
|
142
|
+
job = SendRequestsJob.new(queue, delay: delay, between: between, queue_suffix: queue_suffix, priority: priority)
|
143
|
+
job.enqueue(:wait => delay, :queue => target.queue_name + queue_suffix, :priority => priority)
|
137
144
|
end
|
138
145
|
end
|
139
146
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
|
4
|
+
|
5
|
+
def serialize?(arg)
|
6
|
+
arg.is_a? Fragmentary::Handler
|
7
|
+
end
|
8
|
+
|
9
|
+
def serialize(handler)
|
10
|
+
super(
|
11
|
+
{
|
12
|
+
:class_name => handler.class.name,
|
13
|
+
:args => handler.args
|
14
|
+
}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(hsh)
|
19
|
+
hsh[:class_name].constantize.new(hsh[:args])
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class RequestQueueSerializer < ActiveJob::Serializers::ObjectSerializer
|
4
|
+
|
5
|
+
def serialize?(arg)
|
6
|
+
arg.is_a? Fragmentary::RequestQueue
|
7
|
+
end
|
8
|
+
|
9
|
+
def serialize(queue)
|
10
|
+
super(
|
11
|
+
{
|
12
|
+
:user_type => queue.user_type,
|
13
|
+
:host_root_url => queue.host_root_url,
|
14
|
+
:requests => queue.requests.map do |r|
|
15
|
+
{
|
16
|
+
:method => r.method,
|
17
|
+
:path => r.path,
|
18
|
+
:parameters => r.parameters,
|
19
|
+
:opinions => r.options
|
20
|
+
}
|
21
|
+
end
|
22
|
+
}
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def deserialize(hsh)
|
27
|
+
queue = RequestQueue.new(hsh[:user_type], hsh[:host_root_url])
|
28
|
+
hsh[:requests].each do |r|
|
29
|
+
queue << Request.new(r[:method], r[:path], r[:parameters], r[:options] || {})
|
30
|
+
end
|
31
|
+
queue
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -22,7 +22,7 @@ module Fragmentary
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def session_host
|
25
|
-
@session_host ||= @target.host + (port=@target.port ? ":#{port}" : "")
|
25
|
+
@session_host ||= @target.host + ((port=@target.port) ? ":#{port}" : "")
|
26
26
|
end
|
27
27
|
|
28
28
|
def session_sign_in_path
|
@@ -83,8 +83,10 @@ module Fragmentary
|
|
83
83
|
options.merge!({:params => parameters})
|
84
84
|
options.merge!(session_options)
|
85
85
|
if options.try(:[], :xhr)
|
86
|
+
puts " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
86
87
|
Rails.logger.info " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
87
88
|
else
|
89
|
+
puts " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
88
90
|
Rails.logger.info " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
|
89
91
|
end
|
90
92
|
@session.send(method, path, options)
|
data/lib/fragmentary/version.rb
CHANGED
data/lib/fragmentary.rb
CHANGED
@@ -11,6 +11,8 @@ require 'fragmentary/session_user'
|
|
11
11
|
require 'fragmentary/widget_parser'
|
12
12
|
require 'fragmentary/widget'
|
13
13
|
require 'fragmentary/publisher'
|
14
|
+
require 'fragmentary/jobs/send_requests_job'
|
15
|
+
require 'fragmentary/jobs/dispatch_handlers_job'
|
14
16
|
|
15
17
|
module Fragmentary
|
16
18
|
def self.config
|
@@ -19,4 +21,12 @@ module Fragmentary
|
|
19
21
|
@config
|
20
22
|
end
|
21
23
|
class << self; alias setup config; end
|
24
|
+
|
25
|
+
def self.application
|
26
|
+
Rails.application
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.application_root_url
|
30
|
+
application.routes.url_helpers.root_url
|
31
|
+
end
|
22
32
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fragmentary
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Thomson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-11-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,28 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: delayed_job_active_record
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '4.1'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '4.1'
|
26
|
+
version: '6.0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: wisper-activerecord
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,13 +128,16 @@ files:
|
|
142
128
|
- fragmentary.gemspec
|
143
129
|
- lib/fragmentary.rb
|
144
130
|
- lib/fragmentary/config.rb
|
145
|
-
- lib/fragmentary/dispatcher.rb
|
146
131
|
- lib/fragmentary/fragment.rb
|
147
132
|
- lib/fragmentary/fragments_helper.rb
|
148
133
|
- lib/fragmentary/handler.rb
|
134
|
+
- lib/fragmentary/jobs/dispatch_handlers_job.rb
|
135
|
+
- lib/fragmentary/jobs/send_requests_job.rb
|
149
136
|
- lib/fragmentary/publisher.rb
|
150
137
|
- lib/fragmentary/request.rb
|
151
138
|
- lib/fragmentary/request_queue.rb
|
139
|
+
- lib/fragmentary/serializers/handler_serializer.rb
|
140
|
+
- lib/fragmentary/serializers/request_queue_serializer.rb
|
152
141
|
- lib/fragmentary/session_user.rb
|
153
142
|
- lib/fragmentary/subscriber.rb
|
154
143
|
- lib/fragmentary/subscription.rb
|
@@ -175,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
164
|
- !ruby/object:Gem::Version
|
176
165
|
version: '0'
|
177
166
|
requirements: []
|
178
|
-
rubygems_version: 3.0.
|
167
|
+
rubygems_version: 3.0.9
|
179
168
|
signing_key:
|
180
169
|
specification_version: 4
|
181
170
|
summary: Fragment modeling and caching for Rails
|