From f50a4687520d471f514b669ff251d63eb70aec9b Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 01/17] Simplify the logic, make all the clients consistent, implement deep cleaning The main simplification is "an item exists exactly when it's data key exists". Everything else is then built upon that rule, as described in the README. - The new logic has much better handling of items with duplicated item IDs (it doesn't require transactions as the implementation in 11abffe02babb1df39dc044b0bd2971d8fd61d85 did) - The idea of "deep cleaning" is now much more simple, and also actually implemented! - There's now correct handling of items in queues without data. --- README.md | 178 ++++++++++-- dotnet/RedisWorkQueue.pdf | Bin 146595 -> 152419 bytes dotnet/RedisWorkQueue/README.md | 42 +-- dotnet/RedisWorkQueue/RedisWorkQueue.csproj | 6 +- dotnet/RedisWorkQueue/WorkQueue.cs | 171 ++++++++---- go/WorkQueue.go | 179 +++++++++--- go/go.mod | 6 +- go/go.sum | 6 + node/package.json | 2 +- node/src/KeyPrefix.ts | 2 +- node/src/WorkQueue.ts | 258 ++++++++++-------- python/README.md | 8 + python/pyproject.toml | 2 +- python/redis_work_queue/workqueue.py | 201 ++++++++------ rust/Cargo.toml | 4 +- rust/src/lib.rs | 118 ++++---- scripts/clear-leases | 2 +- tests/dotnet/RedisWorkQueueTests/Program.cs | 17 +- .../RedisWorkQueueTests.csproj | 4 +- tests/go/go.mod | 8 +- tests/go/go.sum | 20 +- tests/go/main.go | 18 +- tests/node/index.ts | 13 +- tests/python-tests.py | 12 +- tests/rust/Cargo.lock | 75 +++-- tests/rust/Cargo.toml | 4 +- tests/rust/src/main.rs | 41 ++- 27 files changed, 910 insertions(+), 487 deletions(-) diff --git a/README.md b/README.md index 65d3284..ca2d06a 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ A work queue, on top of a redis database, with implementations in Python, Rust, Go, Node.js (TypeScript) and Dotnet (C#). -This provides no method of tracking the outcome of work items. This is fairly simple to implement -yourself (just store the result in the redis database with a key derived from the work item id). If -you want a more fully-featured system for managing jobs, see our [Collection +This provides no method of tracking the outcome of work items. Tracking results is fairly simple to +implement yourself (just store the result in the redis database with a key derived from the work +item id). If you want a more fully-featured system for managing jobs, see our [Collection Manager](https://github.com/MeVitae/redis-collection-manager). Implementations in other languages are welcome, open a PR! @@ -43,8 +43,6 @@ All the implementations share the same operations, on the same core types, these Items in the work queue consist of an `id`, a string, and some `data`, arbitrary bytes. For convenience, the IDs are often randomly generated UUIDs, however they can be customized. -Another item with the same ID as a previous item shouldn't be added until the previous item has been -completed. ### Adding an item @@ -57,6 +55,28 @@ Adding an item is exactly what it sounds like! It adds an item to the work queue either be in the queue or being processed (before coming back to the queue if the processing fails) until the job is completed. +#### Adding known unique items (faster) + +*Python: `WorkQueue.add_unique_item`, +Rust: [`WorkQueue::add_unique_item`](https://docs.rs/redis-work-queue/latest/redis_work_queue/struct.WorkQueue.html#method.add_unique_item), +Node.js: [`WorkQueue::add_item`](WorkQueue-addItem), +Go: [`WorkQueue.AddUniqueItem`](https://pkg.go.dev/github.com/mevitae/redis-work-queue/go#WorkQueue.AddUniqueItem)* + +If you know that an item ID is not already in the queue, you can instead use an optimised +`add_unique_item` method, which skips that exact check. + +*If you use this incorrectly, nothing will go too badly wrong, but the reported queue length, which +may be used for autoscaling, will be inaccurate, and leasing items will take multiple iterations.* + +#### Using the right add method + +The default item constructors set the item ID to a randomly generated UUID (universally *unique* +ID). If this is used, then the `add_unique_item` method should be preferred. + +However, if duplicate jobs are likely to be added, then the item IDs should be set such that equal +jobs have equal IDs (for example by using a hash of the job), and then the `add_item` method should +be used, to prevent jobs from being duplicated. + ### Leasing an item *Python: `WorkQueue.lease`, @@ -82,7 +102,7 @@ successfully working, a job will always be run to completion (even if it is run that process). If you're unhappy about jobs being run more than once, see [But I never want my job to run more than -once](#). +once](#but-i-never-want-my-job-to-run-more-than-once). #### Storing the result of a work item @@ -152,18 +172,23 @@ complete will return `true` for only one worker. #### Storing the result -See [Storing the result of a work item](#) +See [Storing the result of a work item](#storing-the-result-of-a-work-item) ### Cleaning +When workers fail to complete items, or if they fail in the middle of redis operations, they can +leave the queue in a state that requires cleaning to ensure items are completed. Because of this, +cleans should occur periodically. + +The frequency and schedule of these is entirely up to you. Light cleans are quick, and can be +carried out regularly. Deep cleans get get very slow depending on the size of your queues, and so +should be performed less often, but should be performed to clean up cases where workers or cleaners +have unexpectedly terminated in the middle of redis operations. + #### Light cleaning *Python: `WorkQueue.light_clean`, Rust implementation planned, no Go or C# implementation planned* -When a worker dies while processing a job, or abandons a job, the job is left in the processing -state until it expires. The role of *light cleaning* is to move these jobs back to the main work -queue so another worker can pick them up. - The interval *light cleaning* should be run on should be approximately equal to the shortest lease time you use. @@ -171,19 +196,14 @@ time you use. *Python and Rust implementations planned, no Go or C# implementation planned* -In addition to this, a worker dying in the middle of a call to `complete` can leave database items -that are no longer associated with an active job. The job of a *deep clean* is to iterate over these -keys and make sure the database is clean. - It's very rare that deep cleaning is needed, but it can happen if you get really unlucky, so it -should be run automatically but infrequently. - -The cleaning process we provide runs this every 6 hours by default. +should be run automatically but less frequently, depending on your requirements for guaranteed +completion times. #### Cleaning process When there are many workers of different types, it's simpler just to have a dedicated process -running the cleaning. We provide a simple cleaner, both in Python and Rust. +running the cleaning. ### Other operations @@ -202,9 +222,127 @@ Node.js: [`WorkQueue.processing`](WorkQueue.processing), Go: [`WorkQueue.QueueLen`](https://pkg.go.dev/github.com/mevitae/redis-work-queue/go#WorkQueue.Processing)* This includes items being worked on and abandoned items (see [Handling errors](#handling-errors)) yet to be -returned to the main queue. +returned to the *main queue*. ## Testing The client implementations each have their own (very simple) unit tests. Most of the testing is done through the integrations tests, located in the [tests](./tests/) directory. + +## Technical details + +A queue is identified by its key prefix. + +Each queue item has a unique ID, and has its own *data key*, which is `:item:`. The +item data is stored with this key. If this key exists, then the item is considered incomplete. If +the item data key does not exist, the item is considered completed. + +The work queue then has a pair of lists, the *main queue* (`:queue`) and *processing queue* +(`:processing`), to track these items. However, if an item ends up in none of these queues +(which can happen if operations aren't properly completed), it is still considered an item. The +*deep clean* process fixes cases like this periodically. + +An item in a queue list, but without a data key, isn't considered an item, so should be ignored and +removed from the queues when it's encountered. + +More specifically, the *main queue* holds the list of item IDs which are yet to be processed. New +items are pushed to the left of the list, and leased items are popped from the right. + +### Adding an item + +To add an item, you must: + +- Store the item data to the item data key (`:item:`), then +- Push the item to the left of the *main queue*. + - The push should be done second, to prevent waiting workers from popping the item immediately, + before the data key is set. + +If the item's data key already exists, you shouldn't push it to the *main queue* again, since this +will cause it to be counted twice when getting the queue length, which can have negative impact on +queue-length based autoscaling. Furthermore, when the item is completed, it's copy will still be in +the *main queue*, so leasing will take more iterations. + +If the ID is already known to be unique (for example UUIDs), you can safely pipeline these +operations and skip the check. + +### Leasing an item + +The fetch an item to work on, a worker should pop from the right of the *main queue*, and push to the +left of the *processing queue* (`rpoplpush`). + +The *processing queue* is a method used to track the items currently being processed, to make the +cleaning process more efficient. + +Furthermore, while an item is being processed, it has a *lease key*, which is +`:lease:`. The value of this is the session ID of the worker which got the lease. +Lease keys are set with an expiry, once the key has expired (or is otherwise deleted), the session +is deemed to have failed working on the item, and the item will be added, by the cleaning process, +to the end of the *main queue*. + +The lease function should therefore: + +- `rpoplpush` from the *main queue* to the processing queue, +- Load the data for this item + - and, if it doesn't exist, ignore the item, + - *(in our provided client implementations, if lease is requested to block, and there's no + timeout, the lease method just tries to get the next item. In any other case, the method returns + with no item)*, then +- set the *lease key*, with an expiry time longer than the expected job duration. + +### Completing an item + +When completing an item, you must: +- Remove the data key. + +You should also: +- Remove the *lease key*, and +- Remove the item ID from the *processing queue*. + +The item is considered to be removed exactly when the data key is removed, but the other steps keep +things tidy (without removing the lease, it would eventually expire, and the cleaning process would +later remove the item ID from the processing queue anyway). + +The completion methods also return a boolean, for which only one remove call must return true. This +boolean can be decided by the output of the command to delete the data key. If it deletes the item, +this is the call that completed the item. Otherwise, another worker has already completed it. + +### Cleaning + +Items are considered items only when their data key exists. + +If a lease does not exist for the item, then processing must have failed before the item was +completed, and the item should be available again for a lease. + +The cleaning process finds items: + +- That exist, +- Aren't in the *main queue*, and +- Don't have a lease. + +And then: + +- Removes them from the processing queue, and +- Pushes them to the *main queue*. + +The item should be removed from the processing queue first. + +#### Deep clean + +The deep cleaning process is complete. It uses `keys` to enumerate all the data items for checking. + +#### Light clean + +Using `keys` can cause significant performance issues, so should ideally be avoided. + +This is why we have the *processing queue*. So long as all operations complete fully, any item with +an expired lease will be in the processing queue, we can therefore follow the usual cleaning +algorithm, but instead only use the item IDs from the *processing queue*. + +#### Cleaning schedule + +This is entirely up to you. Light cleans are quick, and can be carried out regularly. Deep cleans +get get very slow depending on the size of your queues, and so should be performed less often, but +should be performed to clean up cases where workers or cleaners have unexpectedly terminated in the +middle of redis operations. + +*Light cleaning* should be run on should be approximately equal to the shortest lease time you use. diff --git a/dotnet/RedisWorkQueue.pdf b/dotnet/RedisWorkQueue.pdf index d5fda4d48d0c94ebd1814ae10836c157d5e63ea9..8e17eddf60d41458c4308b50de0f6d6af478c29c 100644 GIT binary patch delta 107510 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 delta 102215 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

BnDu7V2$|Qv>F_din1TCxz_-i) zM^{LIHvHC;yK-)7)JE7wc;{z&!e7yrb7uzq&qxu@+pU{Lr!>RmakZ;=1?;?mE9mJ0N}HfVF`Wc4{bPSe5S#XX zG@b@Up4G;kv3OkEWj|LQM)@~bqP1H0=9A@Jw3fjcxyMhsEar;0hIa$TeCt6uQRCHp ze(pM}buNSf<9ia`^&&Y~Dm3J;L{+@g@7!C*yWWdATl4*g?^uSsiw+Y6Jx6Yt6Ixo@ Onz{mtie@*=1^x>XTQAQ5 diff --git a/dotnet/RedisWorkQueue/README.md b/dotnet/RedisWorkQueue/README.md index f56c35d..0dd0432 100644 --- a/dotnet/RedisWorkQueue/README.md +++ b/dotnet/RedisWorkQueue/README.md @@ -9,46 +9,12 @@ readme](https://github.com/MeVitae/redis-work-queue/blob/main/README.md). ## Documentation -Below is a brief overview and an example. More details on the core concepts can be found in the -[readme](https://github.com/MeVitae/redis-work-queue/blob/main/README.md), and full API -documentation can be found in +Below is a brief example. More details on the core concepts can be found in the +[readme](https://github.com/MeVitae/redis-work-queue/blob/main/README.md), and +full API documentation can be found in [../RedisWorkQueue.pdf](https://github.com/MeVitae/redis-work-queue/blob/main/dotnet/RedisWorkQueue.pdf). -## WorkQueue - -The `WorkQueue` class represents a work queue backed by a Redis database. It provides methods to add -items to the queue, lease items from the queue, and mark completed items as done. - -### Properties - -- `Session`: Gets or sets the unique identifier for the current session. - -### Methods - -- `AddItem(IRedisClient db, Item item)`: Adds an item to the work queue. The `db` parameter is the - Redis instance and the `item` parameter is the item to be added. - -- `QueueLength(IRedisClient db)`: Gets the length of the main queue. The `db` parameter is the Redis - instance. - -- `Processing(IRedisClient db)`: Gets the length of the processing queue. The `db` parameter is the - Redis instance. - -- `LeaseExists(IRedisClient db, string itemId)`: Checks if a lease exists for the specified item ID. - The `db` parameter is the Redis instance and the `itemId` parameter is the ID of the item to - check. - -- `Lease(IRedisClient db, int leaseSeconds, bool block, int timeout = 0)`: Requests a work lease - from the work queue. This should be called by a worker to get work to complete. The `db` parameter - is the Redis instance, the `leaseSeconds` parameter is the number of seconds to lease the item for, - the `block` parameter indicates whether to block and wait for an item to be available if the main - queue is empty, and the `timeout` parameter is the maximum time to block in seconds. - -- `Complete(IRedisClient db, Item item)`: Marks a job as completed and removes it from the work - queue. The `db` parameter is the Redis instance and the `item` parameter is the item to be - completed. - -### Example Usage +## Example Usage ```csharp using FreeRedis; diff --git a/dotnet/RedisWorkQueue/RedisWorkQueue.csproj b/dotnet/RedisWorkQueue/RedisWorkQueue.csproj index fd0346f..e2872a3 100644 --- a/dotnet/RedisWorkQueue/RedisWorkQueue.csproj +++ b/dotnet/RedisWorkQueue/RedisWorkQueue.csproj @@ -1,10 +1,10 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 enable MeVitae.RedisWorkQueue - 0.2.1 + 0.3.0 Jacob O'Toole, Nathan Lamplough, Ilie Mihai Alexandru MeVitae https://github.com/MeVitae/redis-work-queue @@ -22,7 +22,7 @@ - + diff --git a/dotnet/RedisWorkQueue/WorkQueue.cs b/dotnet/RedisWorkQueue/WorkQueue.cs index b1e1687..d900d4c 100644 --- a/dotnet/RedisWorkQueue/WorkQueue.cs +++ b/dotnet/RedisWorkQueue/WorkQueue.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Linq; using FreeRedis; @@ -44,24 +45,40 @@ public WorkQueue(KeyPrefix name) this.Session = name.Of(Guid.NewGuid().ToString()); this.MainQueueKey = name.Of(":queue"); this.ProcessingKey = name.Of(":processing"); - this.LeaseKey = name.Concat(":leased_by_session:"); + this.LeaseKey = name.Concat(":lease:"); this.ItemDataKey = name.Concat(":item:"); } ///

- /// Adds item to the work queue. + /// Add an item to the work queue. + /// + /// If an item with the same ID already exists, this item is not added, and `false` is returned. Otherwise, if the item is added `true` is returned. + /// + /// If you know the item ID is unique, and not already in the queue, use the optimised WorkQueue.AddUniqueItem instead. /// /// Redis instance. /// Item to be added. - public void AddItem(IRedisClient db, Item item) + public bool AddItem(IRedisClient db, Item item) { - using (var pipe = db.StartPipe()) + if (db.SetNx(ItemDataKey.Of(item.ID), item.Data)) { - pipe.Set(ItemDataKey.Of(item.ID), item.Data); - pipe.LPush(MainQueueKey, item.ID); - - pipe.EndPipe(); + db.LPush(MainQueueKey, item.ID); + return true; } + return false; + } + + /// + /// Add an item, which is known to have an ID not already in the queue. + /// + /// Redis instance. + /// Item to be added. + public void AddUniqueItem(IRedisClient db, Item item) + { + using var pipe = db.StartPipe(); + pipe.Set(ItemDataKey.Of(item.ID), item.Data); + pipe.LPush(MainQueueKey, item.ID); + pipe.EndPipe(); } /// @@ -90,7 +107,7 @@ public long Processing(IRedisClient db) /// The Redis client instance. /// The ID of the item to check. /// True if lease exists, false otherwise. - public bool LeaseExists(IRedisClient db, string itemId) + private bool LeaseExists(IRedisClient db, string itemId) { return db.Exists(LeaseKey.Of(itemId)); } @@ -98,74 +115,130 @@ public bool LeaseExists(IRedisClient db, string itemId) /// /// Request a work lease from the work queue. This should be called by a worker to get work to complete. + /// /// When completed, the `complete` method should be called. + /// /// If `block` is true, the function will return either when a job is leased or after `timeout` seconds if `timeout` isn't 0. + /// /// If the job is not completed before the end of `leaseDuration`, another worker may pick up the same job. + /// /// It is not a problem if a job is marked as `done` more than once. - ///If you haven't already, it's worth reading the documentation on leasing items: + /// + /// If you haven't already, it's worth reading the documentation on leasing items: /// https://github.com/MeVitae/redis-work-queue/blob/main/README.md#leasing-an-item /// /// The Redis client instance. /// The number of seconds to lease the item for. /// Indicates whether to block and wait for an item to be available if the main queue is empty. - /// The maximum time to block in seconds. + /// The maximum time to block in seconds. If 0, there is not timeout. /// The leased item, or null if no item is available. public Item? Lease(IRedisClient db, int leaseSeconds, bool block, int timeout = 0) { - object maybeItemId; - if (block) - { - maybeItemId = db.BRPopLPush(MainQueueKey, ProcessingKey, timeout); - } - else + for (; ; ) { - maybeItemId = db.RPopLPush(MainQueueKey, ProcessingKey); + object maybeItemId; + if (block) + maybeItemId = db.BRPopLPush(MainQueueKey, ProcessingKey, timeout); + else + maybeItemId = db.RPopLPush(MainQueueKey, ProcessingKey); + + if (maybeItemId == null) + return null; + + string itemId; + if (maybeItemId is byte[]) + itemId = Encoding.UTF8.GetString((byte[])maybeItemId); + else if (maybeItemId is string) + itemId = (string)maybeItemId; + else + throw new Exception("item id from work queue not bytes or string"); + + var data = db.Get(ItemDataKey.Of(itemId)); + if (data == null) + { + if (block && timeout == 0) + continue; + return null; + } + + db.SetEx(LeaseKey.Of(itemId), leaseSeconds, Encoding.UTF8.GetBytes(Session)); + + return new Item(data, itemId); } - - if (maybeItemId == null) - return null; - - string itemId; - if (maybeItemId is byte[]) - itemId = Encoding.UTF8.GetString((byte[])maybeItemId); - else if (maybeItemId is string) - itemId = (string)maybeItemId; - else - throw new Exception("item id from work queue not bytes or string"); - - var data = db.Get(ItemDataKey.Of(itemId)); - if (data == null) - data = new byte[0]; - - db.SetEx(LeaseKey.Of(itemId), leaseSeconds, Encoding.UTF8.GetBytes(Session)); - - return new Item(data, itemId); } /// - /// Marks a job as completed and remove it from the work queue. + /// Marks a job as completed and remove it from the work queue. After `complete` has been + /// called (and returns `true`), no workers will receive this job again. /// /// The Redis client instance. /// The item to be completed. /// True if the item was successfully completed and removed, otherwise false. public bool Complete(IRedisClient db, Item item) { - var removed = db.LRem(ProcessingKey, 0, item.ID); - - if (removed == 0) - return false; + using var pipe = db.StartPipe(); + pipe.Del(ItemDataKey.Of(item.ID)); + pipe.LRem(ProcessingKey, 0, item.ID); + pipe.Del(LeaseKey.Of(item.ID)); + var results = pipe.EndPipe(); + return ((long)results[0]) != 0; + } - string itemId = item.ID; + public void LightClean(IRedisClient db) + { + // A light clean only checks items in the processing queue + var processing = db.LRange(ProcessingKey, 0, -1); + foreach (string itemId in processing) + { + // If there's no lease for the item, then it should be reset. + if (!LeaseExists(db, itemId)) + { + // We also check the item actually exists before pushing it back to the main queue + if (db.Exists(ItemDataKey.Of(itemId))) + { + Console.WriteLine($"{itemId} has not lease, it will be reset"); + using var pipe = db.StartPipe(); + pipe.LRem(ProcessingKey, 0, itemId); + pipe.LPush(MainQueueKey, itemId); + pipe.EndPipe(); + } + else + { + Console.WriteLine($"{itemId} was in the processing queue but does not exist"); + db.LRem(ProcessingKey, 0, itemId); + } + } + } + } + public void DeepClean(IRedisClient db) + { + // A deep clean checks all data keys + string[] itemDataKeys; + string[] mainQueue; using (var pipe = db.StartPipe()) { - pipe.Del(ItemDataKey.Of(itemId)); - pipe.Del(LeaseKey.Of(itemId)); - - pipe.EndPipe(); + pipe.Keys(ItemDataKey.Of("*")); + pipe.LRange(MainQueueKey, 0, -1); + var results = pipe.EndPipe(); + itemDataKeys = (string[])results[0]; + mainQueue = (string[])results[1]; + } + var processing = db.LRange(ProcessingKey, 0, -1); + foreach (string itemDataKey in itemDataKeys) + { + string itemId = itemDataKey.Substring(ItemDataKey.Prefix.Length); + // If the item isn't in the queue, and there's no lease for the item, then it should + // be reset. + if (!mainQueue.Contains(itemId) && !LeaseExists(db, itemId)) + { + Console.WriteLine($"{itemId} has not lease, it will be reset"); + using var pipe = db.StartPipe(); + pipe.LRem(ProcessingKey, 0, itemId); + pipe.LPush(MainQueueKey, itemId); + pipe.EndPipe(); + } } - - return true; } } } diff --git a/go/WorkQueue.go b/go/WorkQueue.go index 3286bb3..76765d1 100644 --- a/go/WorkQueue.go +++ b/go/WorkQueue.go @@ -66,6 +66,8 @@ package workqueue import ( "context" + "fmt" + "slices" "time" "github.com/google/uuid" @@ -93,15 +95,28 @@ func NewWorkQueue(name KeyPrefix) WorkQueue { session: uuid.NewString(), mainQueueKey: name.Of(":queue"), processingKey: name.Of(":processing"), - leaseKey: name.Concat(":leased_by_session:"), + leaseKey: name.Concat(":lease:"), itemDataKey: name.Concat(":item:"), } } -// AddItemToPipeline adds an item to the work queue. This adds the redis commands onto the pipeline passed. +// AddItem to the work queue. +// +// If an item with the same ID already exists, this item is not added, and false is returned. Otherwise, if the item is added true is returned. +// +// If you know the item ID is unique, and not already in the queue, use the optimised WorkQueue.AddUniqueItem instead. +func (workQueue *WorkQueue) AddItem(ctx context.Context, db *redis.Client, item Item) (bool, error) { + added, err := db.SetNX(ctx, workQueue.itemDataKey.Of(item.ID), item.Data, never).Result() + if added { + err = db.LPush(ctx, workQueue.mainQueueKey, item.ID).Err() + } + return added, err +} + +// AddItemToPipeline adds an item, which is known to have an ID not already in the queue, to the work queue. This adds the redis commands onto the pipeline passed. // -// Use [WorkQueue.AddItem] if you don't want to pass a pipeline directly. -func (workQueue *WorkQueue) AddItemToPipeline(ctx context.Context, pipeline redis.Pipeliner, item Item) { +// Use [WorkQueue.AddUniqueItem] if you don't want to pass a pipeline directly. +func (workQueue *WorkQueue) AddUniqueItemToPipeline(ctx context.Context, pipeline redis.Pipeliner, item Item) { // Add the item data // NOTE: it's important that the data is added first, otherwise someone could pop the item // before the data is ready @@ -110,12 +125,12 @@ func (workQueue *WorkQueue) AddItemToPipeline(ctx context.Context, pipeline redi pipeline.LPush(ctx, workQueue.mainQueueKey, item.ID) } -// AddItem to the work queue. +// AddItem, which is known to have an ID not already in the queue, to the work queue. // // This creates a pipeline and executes it on the database. -func (workQueue *WorkQueue) AddItem(ctx context.Context, db *redis.Client, item Item) error { +func (workQueue *WorkQueue) AddUniqueItem(ctx context.Context, db *redis.Client, item Item) error { pipeline := db.Pipeline() - workQueue.AddItemToPipeline(ctx, pipeline, item) + workQueue.AddUniqueItemToPipeline(ctx, pipeline, item) _, err := pipeline.Exec(ctx) return err } @@ -151,36 +166,119 @@ func (workQueue *WorkQueue) Lease( timeout time.Duration, leaseDuration time.Duration, ) (*Item, error) { - // First, to get an item, we try to move an item from the main queue to the processing list. - var command *redis.StringCmd - if block { - command = db.BRPopLPush(ctx, workQueue.mainQueueKey, workQueue.processingKey, timeout) - } else { - command = db.RPopLPush(ctx, workQueue.mainQueueKey, workQueue.processingKey) - } - itemId, err := command.Result() - if itemId == "" || err != nil { - // A nil error indicates no job available - if err == redis.Nil { - return nil, nil + for { + // First, to get an item, we try to move an item from the main queue to the processing list. + var command *redis.StringCmd + if block { + command = db.BRPopLPush(ctx, workQueue.mainQueueKey, workQueue.processingKey, timeout) + } else { + command = db.RPopLPush(ctx, workQueue.mainQueueKey, workQueue.processingKey) } - return nil, err + itemId, err := command.Result() + if itemId == "" || err != nil { + // A nil error indicates no job available + if err == redis.Nil { + return nil, nil + } + return nil, err + } + + // Get the item's data + data, err := db.Get(ctx, workQueue.itemDataKey.Of(itemId)).Bytes() + if err != nil { + if err == redis.Nil { + if block && timeout == 0 { + continue + } + return nil, nil + } + return nil, err + } + + // Now setup the lease item. + // NOTE: Racing for a lease is ok + err = db.SetEx(ctx, workQueue.leaseKey.Of(itemId), workQueue.session, leaseDuration).Err() + + return &Item{ + ID: itemId, + Data: data, + }, err } +} + +func (workQueue *WorkQueue) leaseExists(ctx context.Context, db *redis.Client, itemId string) (bool, error) { + exists, err := db.Exists(ctx, workQueue.itemDataKey.Of(itemId)).Result() + return exists > 0, err +} - // Get the item's data - data, err := db.Get(ctx, workQueue.itemDataKey.Of(itemId)).Bytes() +func (workQueue *WorkQueue) LightClean(ctx context.Context, db *redis.Client) error { + // A light clean only checks items in the processing queue + itemIds, err := db.LRange(ctx, workQueue.processingKey, 0, -1).Result() if err != nil { - return nil, err + return fmt.Errorf("failed to list processing queue: %w", err) } + for _, itemId := range itemIds { + leaseExists, err := workQueue.leaseExists(ctx, db, itemId) + if err != nil { + return fmt.Errorf("failed to check if lease exists for %s: %w", itemId, err) + } + // If there's no lease for the item, then it should be reset. + if !leaseExists { + // We also check that the item actually exists before pushing it back to the main queue + itemExists, err := db.Exists(ctx, workQueue.itemDataKey.Of(itemId)).Result() + if err != nil { + return fmt.Errorf("failed to check if item %s exists: %w", itemId, err) + } + if itemExists != 0 { + fmt.Println(itemId, "has no lease, it will be reset") + pipeline := db.Pipeline() + pipeline.LRem(ctx, workQueue.processingKey, 0, itemId) + pipeline.LPush(ctx, workQueue.mainQueueKey, itemId) + _, err := pipeline.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to move %s from processing queue to main queue: %w", itemId, err) + } + } else { + fmt.Println(itemId, "was in the processing queue but does not exist") + err = db.LRem(ctx, workQueue.processingKey, 0, itemId).Err() + if err != nil { + return fmt.Errorf("failed to remove %s from processing queue: %w", itemId, err) + } + } + } + } + return nil +} - // Now setup the lease item. - // NOTE: Racing for a lease is ok - err = db.SetEx(ctx, workQueue.leaseKey.Of(itemId), workQueue.session, leaseDuration).Err() - - return &Item{ - ID: itemId, - Data: data, - }, err +func (workQueue *WorkQueue) DeepClean(ctx context.Context, db *redis.Client) error { + // A deep clean checks all data keys + pipeline := db.Pipeline() + itemDataKeys := db.Keys(ctx, workQueue.itemDataKey.Of("*")) + mainQueueRes := db.LRange(ctx, workQueue.mainQueueKey, 0, -1) + _, err := pipeline.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to list item data keys: %w", err) + } + mainQueue := mainQueueRes.Val() + for _, itemDataKey := range itemDataKeys.Val() { + itemId := itemDataKey[:len(workQueue.itemDataKey)] + leaseExists, err := workQueue.leaseExists(ctx, db, itemId) + if err != nil { + return fmt.Errorf("failed to check if lease exists for %s: %w", itemId, err) + } + // If the item isn't in the queue, and there's no lease for the item, then it should be reset. + if !slices.Contains(mainQueue, itemId) && !leaseExists { + fmt.Println(itemId, "has no lease, it will be reset") + pipeline := db.Pipeline() + pipeline.LRem(ctx, workQueue.processingKey, 0, itemId) + pipeline.LPush(ctx, workQueue.mainQueueKey, itemId) + _, err := pipeline.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to move %s from processing queue to main queue: %w", itemId, err) + } + } + } + return nil } // Complete marks a job as completed and remove it from the work queue. After Complete has been @@ -190,17 +288,10 @@ func (workQueue *WorkQueue) Lease( // first worker to call Complete*. So, while lease might give the same job to multiple workers, // complete will return true for only one worker. func (workQueue *WorkQueue) Complete(ctx context.Context, db *redis.Client, item *Item) (bool, error) { - removed, err := db.LRem(ctx, workQueue.processingKey, 0, item.ID).Result() - if removed == 0 || err != nil { - return false, err - } - // If we did actually remove it, delete the item data and lease. - // If we didn't really remove it, it's probably been returned to the work queue so the data is - // still needed and the lease might not be ours (if it is still ours, it'll expire anyway). - _, err = db.Pipelined(ctx, func(pipeline redis.Pipeliner) error { - pipeline.Del(ctx, workQueue.itemDataKey.Of(item.ID)) - pipeline.Del(ctx, workQueue.leaseKey.Of(item.ID)) - return nil - }) - return true, err + pipeline := db.Pipeline() + delResult := pipeline.Del(ctx, workQueue.itemDataKey.Of(item.ID)) + pipeline.LRem(ctx, workQueue.processingKey, 0, item.ID) + pipeline.Del(ctx, workQueue.leaseKey.Of(item.ID)) + _, err := pipeline.Exec(ctx) + return delResult.Val() != 0, err } diff --git a/go/go.mod b/go/go.mod index ebce525..7663450 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,11 +3,11 @@ module github.com/mevitae/redis-work-queue/go go 1.20 require ( - github.com/google/uuid v1.4.0 - github.com/redis/go-redis/v9 v9.3.0 + github.com/google/uuid v1.6.0 + github.com/redis/go-redis/v9 v9.6.1 ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/go/go.sum b/go/go.sum index c34322c..f313488 100644 --- a/go/go.sum +++ b/go/go.sum @@ -2,9 +2,15 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= diff --git a/node/package.json b/node/package.json index c1127bd..7f899c7 100644 --- a/node/package.json +++ b/node/package.json @@ -1,6 +1,6 @@ { "name": "@mevitae/redis-work-queue", - "version": "0.1.5", + "version": "0.3.0", "main": "./src/WorkQueue.ts", "description": "A work queue, on top of a redis database, with implementations in Python, Rust, Go, Node.js (TypeScript) and Dotnet (C#).", "keywords": [ diff --git a/node/src/KeyPrefix.ts b/node/src/KeyPrefix.ts index 62a05e4..e537ed3 100644 --- a/node/src/KeyPrefix.ts +++ b/node/src/KeyPrefix.ts @@ -18,7 +18,7 @@ export class KeyPrefix { /** * KeyPrefix instance prefix. */ - private prefix: string; + prefix: string; /** * This creates a new instance with the prefix passed. diff --git a/node/src/WorkQueue.ts b/node/src/WorkQueue.ts index 5fb59ab..c72880f 100644 --- a/node/src/WorkQueue.ts +++ b/node/src/WorkQueue.ts @@ -17,7 +17,6 @@ export class WorkQueue { private session: string; private mainQueueKey: string; private processingKey: string; - private cleaningKey: string; private leaseKey: KeyPrefix; private itemDataKey: KeyPrefix; @@ -27,20 +26,40 @@ export class WorkQueue { constructor(name: KeyPrefix) { this.mainQueueKey = name.of(':queue'); this.processingKey = name.of(':processing'); - this.cleaningKey = name.of(':cleaning'); this.session = uuidv4(); - this.leaseKey = name.concat(':leased_by_session:'); + this.leaseKey = name.concat(':lease:'); this.itemDataKey = name.concat(':item:'); } /** - * Add an item to the work queue. This adds the redis commands onto the pipeline passed. - * Use `WorkQueue.addItem` if you don't want to pass a pipeline directly. - * Add the item data. - * @param {Pipeline} pipeline The pipeline that the data will be executed. - * @param {Item} item The Item which will be set in the Redis with the key of this.itemDataKey.of(item.id). + * Add an item to the work queue. + * + * If an item with the same ID already exists, this item is not added, and `false` is returned. + * Otherwise, if the item is added `true` is returned. + * + * If you know the item ID is unique, and not already in the queue, use the optimised + * `WorkQueue.add_unique_item` instead. + * + * @param {Redis} db - The Redis Connection. + * @param {Item} item - The item to be added.. + * @returns {Promise} - A boolean indicating if the item was added. */ - addItemToPipeline(pipeline: ChainableCommander, item: Item) { + async addItem(db: Redis, item: Item): Promise { + const added = await db.setnx(this.itemDataKey.of(item.id), item.data) > 0; + if (added) await db.lpush(this.mainQueueKey, item.id); + return added; + } + + /** + * Add an item, which is known to have an ID not already in the queue, to the work queue. This + * adds the redis commands onto the pipeline passed. + * + * Use `WorkQueue.add_unique_item` if you don't want to pass a pipeline directly. + * + * @param {Pipeline} pipeline - The pipeline that the data will be executed. + * @param {Item} item - The Item to be added. + */ + addUniqueItemToPipeline(pipeline: ChainableCommander, item: Item) { // NOTE: it's important that the data is added first, otherwise someone before the data is ready. pipeline.set(this.itemDataKey.of(item.id), item.data); // Then add the id to the work queue @@ -48,32 +67,34 @@ export class WorkQueue { } /** - * Add an item to the work queue. + * Add an item, which is known to have an ID not already in the queue, to the work queue. + * * This creates a pipeline and executes it on the database. * - * @param {Redis} db The Redis Connection. - * @param item The item that will be executed using the method addItemToPipeline. + * @param {Redis} db - The Redis Connection. + * @param {Item} item - The item to be added. */ - async addItem(db: Redis, item: Item): Promise { + async addUniqueItem(db: Redis, item: Item): Promise { const pipeline = db.pipeline(); - this.addItemToPipeline(pipeline, item); + this.addUniqueItemToPipeline(pipeline, item); await pipeline.exec(); } /** - * This is used to get the length of the Main Queue. + * Return the length of the work queue (not including items being processed, see + * `WorkQueue.processing`). * - * @param {Redis} db The Redis Connection. - * @returns {Promise} Return the length of the work queue (not including items being processed, see `WorkQueue.processing()`). + * @param {Redis} db - The Redis Connection. + * @returns {Promise} The length of the work queue (not including items being processed). */ queueLen(db: Redis): Promise { return db.llen(this.mainQueueKey); } /** - * This is used to get the lenght of the Processing Queue. + * Return the number of items being processed. * - * @param {Redis} db The Redis Connection. + * @param {Redis} db - The Redis Connection. * @returns {Promise} The number of items being processed. */ processing(db: Redis): Promise { @@ -81,138 +102,147 @@ export class WorkQueue { } /** - * This method can be used to check if a Lease Exists or not for a itemId. + * Request a work lease from the work queue. This should be called by a worker to get work to + * complete. When completed, the `complete` method should be called. * - * @param {Redis} db The Redis Connection. - * @param {string} itemId The itemId of the item you want to check if it has a lease. - * @returns {Promise} - */ - async leaseExists(db: Redis, itemId: string): Promise { - const exists = await db.exists(this.leaseKey.of(itemId)); - return exists !== 0; - } - - /** - * Request a work lease from the work queue. This should be called by a worker to get work to complete. - * When completed, the `complete` method should be called. + * If `block` is true, the function will return either when a job is leased or after `timeout` + * seconds if `timeout` isn't 0. * - * If `block` is true, the function will return either when a job is leased or after `timeout` seconds if `timeout` isn't 0. * If the job is not completed before the end of `leaseDuration`, another worker may pick up the same job. + * * It is not a problem if a job is marked as `done` more than once. * * If you haven't already, it's worth reading the documentation on leasing items: * https://github.com/MeVitae/redis-work-queue/blob/main/README.md#leasing-an-item * - * @param {Redis} db The Redis Connection. - * @param {number} leaseSecs The number of seconds that the lease should hold. - * @param {boolean} block Is a block or not, default is true. - * @param {number} timeout The number of seconds the lease will time out at. - * @returns {Promise} Returns a new lease Item. - * - * Process: - * First, to get an item, we try to move an item from the main queue to the processing list. - * Then we setup the lease item. + * @param {Redis} db - The Redis Connection. + * @param {number} leaseSecs - The number of seconds that the lease should hold. + * @param {boolean} block - If false, the method will return immediately if there is no item. + * @param {number} timeout - The number of seconds the lease will time out at. + * @returns {Promise} The item to process! */ async lease( db: Redis, leaseSecs: number, block = true, - timeout = 1 + timeout = 0, ): Promise { - let maybeItemId: string | null = null; - - // Try to move an item from the main queue to the processing list. - if (block) { - maybeItemId = await db.brpoplpush( - this.mainQueueKey, - this.processingKey, - timeout - ); - } else { - maybeItemId = await db.rpoplpush(this.mainQueueKey, this.processingKey); - } + for (;;) { + let maybeItemId: string | null = null; + + // Try to move an item from the main queue to the processing list. + if (block) { + maybeItemId = await db.brpoplpush( + this.mainQueueKey, + this.processingKey, + timeout + ); + } else { + maybeItemId = await db.rpoplpush(this.mainQueueKey, this.processingKey); + } - if (maybeItemId == null) { - return null; - } + if (maybeItemId == null) { + return null; + } - const itemId = maybeItemId; + const itemId = maybeItemId; - let data: Buffer | null = await db.getBuffer(this.itemDataKey.of(itemId)); + let data: Buffer | null = await db.getBuffer(this.itemDataKey.of(itemId)); - if (data == null) { - data = Buffer.alloc(0); + if (data == null) { + if (block && timeout === 0) continue; + return null; + } + + // Setup the lease item. + await db.setex(this.leaseKey.of(itemId), leaseSecs, this.session); + return new Item(data, itemId); } + } - // Setup the lease item. - await db.setex(this.leaseKey.of(itemId), leaseSecs, this.session); - return new Item(data, itemId); + /** + * Marks a job as completed and remove it from the work queue. After `complete` has been called + * (and returns `true`), no workers will receive this job again. + * + * @param {Redis} db - The Redis connection. + * @param {Item} item - The Item to complete. + * @returns {boolean} a boolean indicating if *the job has been removed* **and** *this worker was the first worker to call `complete`*. So, while lease might give the same job to multiple workers, complete will return `true` for only one worker. + */ + async complete(db: Redis, item: Item): Promise { + const results = await db.pipeline() + .del(this.itemDataKey.of(item.id)) + .lrem(this.processingKey, 0, item.id) + .del(this.leaseKey.of(item.id)) + .exec(); + return Boolean(results && results[0][1]); } /** - * Moves items from the processing Queue to the Main Queue if the lease key is missing. - * This can be used in case worker dies or crashes and item is hold onto the processing, this allows the item to be moved onto another worker. + * Check if a lease exists for an `itemId`. * - * @param {Redis} db The Redis connection. + * @param {Redis} db - The Redis Connection. + * @param {string} itemId + * @returns {Promise} A boolean indicating if a lease exists for the item. + */ + private async leaseExists(db: Redis, itemId: string): Promise { + const exists = await db.exists(this.leaseKey.of(itemId)); + return exists > 0; + } + + /** + * Check if an item exists with `itemId`. * - * Process Explenation: - * If the lease key is not present for an item (it expired or was never created because the client crashed before creating it), then move the item back to the main queue so others can work on it. - * While working on an item, we store it in the cleaning list. If we ever crash, we come back and check these items. + * @param {Redis} db - The Redis Connection. + * @param {string} itemId + * @returns {Promise} A boolean indicating if the item exists. */ + private async itemExists(db: Redis, itemId: string): Promise { + const exists = await db.exists(this.leaseKey.of(itemId)); + return exists > 0; + } + async lightClean(db: Redis) { const processing: Array = await db.lrange( this.processingKey, - 0, - -1 + 0, -1, ); for (const itemId of processing) { + // If there's no lease for the item, then it should be reset. if (!(await this.leaseExists(db, itemId))) { - await db.lpush(this.cleaningKey, itemId); - const removed = await db.lrem(this.processingKey, 0, itemId); - if (removed > 0) { - await db.lpush(this.mainQueueKey, 0, itemId); + // We also check that the item actually exists before pushing it back to the main queue + if (await this.itemExists(db, itemId)) { + console.log(itemId, "has no lease, it will be reset"); + await db.pipeline() + .lrem(this.processingKey, 0, itemId) + .lpush(this.mainQueueKey, itemId) + .exec(); + } else { + console.log(itemId, "was in the processing queue but does not exist"); + await db.lrem(this.processingKey, 0, itemId); } - await db.lrem(this.cleaningKey, 0, itemId); } } - - const forgot: Array = await db.lrange(this.cleaningKey, 0, -1); - for (const itemId of forgot) { - const leaseExists: boolean = await this.leaseExists(db, itemId); - if ( - !leaseExists && - (await db.lpos(this.mainQueueKey, itemId)) == null && - (await db.lpos(this.processingKey, itemId)) == null - ) { - /** - * FIXME: this introduces a race - * maybe not anymore - * no, it still does, what if the job has been completed? - */ - await db.lpush(this.mainQueueKey, itemId); - } - await db.lrem(this.cleaningKey, 0, itemId); - } } - /** - * Marks a job as completed and remove it from the work queue. - * - * @param {Redis} db The Redis connection. - * @param {Item} item The Item which the processing got completed - * @returns {boolean} returns a boolean indicating if *the job has been removed* **and** *this worker was the first worker to call `complete`*. So, while lease might give the same job to multiple workers, complete will return `true` for only one worker. - */ - async complete(db: Redis, item: Item): Promise { - const removed = await db.lrem(this.processingKey, 0, item.id); - if (removed === 0) { - return false; + async deepClean(db: Redis) { + // A deep clean checks all data keys + const itemResults = await db.pipeline() + .keys(this.itemDataKey.of("*")) + .lrange(this.mainQueueKey, 0, -1) + .exec(); + if (!itemResults) throw new Error("pipeline did not return results"); + const [[_err_a, itemDataKeys], [_err_b, mainQueueUntyped]] = itemResults; + const mainQueue = mainQueueUntyped as string[]; + for (const itemDataKey of itemDataKeys as string[]) { + const itemId = itemDataKey.slice(this.itemDataKey.prefix.length); + // If the item isn't in the queue, and there's no lease for the item, then it should be reset. + if (!mainQueue.includes(itemId) && !(await this.leaseExists(db, itemId))) { + console.log(itemId, "has no lease, it will be reset"); + await db.pipeline() + .lrem(this.processingKey, 0, itemId) + .lpush(this.mainQueueKey, itemId) + .exec(); + } } - - const pipeline = db.pipeline(); - pipeline.del(this.itemDataKey.of(item.id)); - pipeline.del(this.leaseKey.of(item.id)); - await pipeline.exec(); - - return true; } } diff --git a/python/README.md b/python/README.md index f0e53b1..969faef 100644 --- a/python/README.md +++ b/python/README.md @@ -42,10 +42,18 @@ assert bytes_item.data_json() == [1, 2, 3] ``` ### Add an item to a work queue + ```python work_queue.add_item(db, item) ``` +If you know that items have unique IDs, which aren't the same as any already in the queue, you can +instead use: + +```python +work_queue.add_unique_item(db, item) +``` + ## Completing work Please read [the documentation on leasing and completing diff --git a/python/pyproject.toml b/python/pyproject.toml index 752757a..393699a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "redis-work-queue" -version = "0.1.5" +version = "0.3.0" description = "A work queue, on top of a redis database, with implementations in Python, Rust, Go, Node.js (TypeScript) and Dotnet (C#)." dependencies = ["redis", "uuid"] authors = [ diff --git a/python/redis_work_queue/workqueue.py b/python/redis_work_queue/workqueue.py index 79b605e..ac1d530 100644 --- a/python/redis_work_queue/workqueue.py +++ b/python/redis_work_queue/workqueue.py @@ -13,14 +13,25 @@ def __init__(self, name: KeyPrefix): self._session = uuid.uuid4().hex self._main_queue_key = name.of(':queue') self._processing_key = name.of(':processing') - self._cleaning_key = name.of(':cleaning') - self._lease_key = KeyPrefix.concat(name, ':leased_by_session:') + self._lease_key = KeyPrefix.concat(name, ':lease:') self._item_data_key = KeyPrefix.concat(name, ':item:') - def add_item_to_pipeline(self, pipeline: Pipeline, item: Item) -> None: - """Add an item to the work queue. This adds the redis commands onto the pipeline passed. + def add_item(self, db: Redis, item: Item) -> bool: + """Add an item to the work queue. + + If an item with the same ID already exists, this item is not added, and False is returned. Otherwise, if the item is added True is returned. - Use `WorkQueue.add_item` if you don't want to pass a pipeline directly. + If you know the item ID is unique, and not already in the queue, use the optimised `add_unique_item` method instead. + """ + if db.setnx(self._item_data_key.of(item.id()), item.data()) != 0: + db.lpush(self._main_queue_key, item.id()) + return True + return False + + def add_unique_item_to_pipeline(self, pipeline: Pipeline, item: Item) -> None: + """Add an item, which is known to have an ID not already in the queue, to the work queue. This adds the redis commands onto the pipeline passed. + + Use `WorkQueue.add_unique_item` if you don't want to pass a pipeline directly. """ # Add the item data # NOTE: it's important that the data is added first, otherwise someone before the data is @@ -29,13 +40,13 @@ def add_item_to_pipeline(self, pipeline: Pipeline, item: Item) -> None: # Then add the id to the work queue pipeline.lpush(self._main_queue_key, item.id()) - def add_item(self, db: Redis, item: Item) -> None: - """Add an item to the work queue. + def add_unique_item(self, db: Redis, item: Item) -> None: + """Add an item, which is known to have an ID not already in the queue, to the work queue. This creates a pipeline and executes it on the database. """ pipeline = db.pipeline() - self.add_item_to_pipeline(pipeline, item) + self.add_unique_item_to_pipeline(pipeline, item) pipeline.execute() def queue_len(self, db: Redis) -> int: @@ -47,54 +58,13 @@ def processing(self, db: Redis) -> int: """Return the number of items being processed.""" return db.llen(self._processing_key) - def light_clean(self, db: Redis) -> None: - processing: list[bytes | str] = db.lrange( - self._processing_key, 0, -1) - for item_id in processing: - if isinstance(item_id, bytes): - item_id = item_id.decode('utf-8') - # If the lease key is not present for an item (it expired or was never created because - # the client crashed before creating it) then move the item back to the main queue so - # others can work on it. - if not self._lease_exists(db, item_id): - print(item_id, 'has no lease') - # While working on an item, we store it in the cleaning list. If we ever crash, we - # come back and check these items. - db.lpush(self._cleaning_key, item_id) - removed = int(db.lrem(self._processing_key, 0, item_id)) - if removed > 0: - db.lpush(self._main_queue_key, item_id) - print(item_id, 'was still in the processing queue, it was reset') - else: - print(item_id, 'was no longer in the processing queue') - db.lrem(self._cleaning_key, 0, item_id) - - # Now we check the - forgot: list[bytes | str] = db.lrange(self._cleaning_key, 0, -1) - for item_id in forgot: - if isinstance(item_id, bytes): - item_id = item_id.decode('utf-8') - print(item_id, 'was forgotten in clean') - if not self._lease_exists(db, item_id) and \ - db.lpos(self._main_queue_key, item_id) is None and \ - db.lpos(self._processing_key, item_id) is None: - # FIXME: this introcudes a race - # maybe not anymore - # no, it still does, what if the job has been completed? - db.lpush(self._main_queue_key, item_id) - print(item_id, 'was not in any queue, it was reset') - db.lrem(self._cleaning_key, 0, item_id) - - def _lease_exists(self, db: Redis, item_id: str) -> bool: - """True iff a lease on 'item_id' exists.""" - return db.exists(self._lease_key.of(item_id)) != 0 - def lease(self, db: Redis, lease_secs: int, block=True, timeout=0) -> Item | None: """Request a work lease the work queue. This should be called by a worker to get work to complete. When completed, the `complete` method should be called. If `block` is true, the function will return either when a job is leased or after `timeout` - if `timeout` isn't 0. + if `timeout` isn't 0. (If `timeout` isn't 0, this may return earlier, with `None` in some + race cases). If the job is not completed before the end of `lease_duration`, another worker may pick up the same job. It is not a problem if a job is marked as `done` more than once. @@ -102,39 +72,42 @@ def lease(self, db: Redis, lease_secs: int, block=True, timeout=0) -> Item | Non If you've not already done it, it's worth reading [the documentation on leasing items](https://github.com/MeVitae/redis-work-queue/blob/main/README.md#leasing-an-item). """ - # First, to get an item, we try to move an item from the main queue to the processing list. - if block: - maybe_item_id: bytes | str | None = db.brpoplpush( - self._main_queue_key, - self._processing_key, - timeout=timeout, - ) - else: - maybe_item_id: bytes | str | None = db.rpoplpush( - self._main_queue_key, - self._processing_key, - ) - - if maybe_item_id is None: - return None - - # Make sure the item id is a string - if isinstance(maybe_item_id, bytes): - item_id: str = maybe_item_id.decode('utf-8') - elif isinstance(maybe_item_id, str): - item_id: str = maybe_item_id - else: - raise Exception("item id from work queue not bytes or string") - - # If we got an item, fetch the associated data. - data: bytes | None = db.get(self._item_data_key.of(item_id)) - if data is None: - data = bytes() - - # Setup the lease item. - # NOTE: Racing for a lease is ok. - db.setex(self._lease_key.of(item_id), lease_secs, self._session) - return Item(data, id=item_id) + while True: + # First, to get an item, we try to move an item from the main queue to the processing list. + if block: + maybe_item_id: bytes | str | None = db.brpoplpush( + self._main_queue_key, + self._processing_key, + timeout=timeout, + ) + else: + maybe_item_id: bytes | str | None = db.rpoplpush( + self._main_queue_key, + self._processing_key, + ) + + if maybe_item_id is None: + return None + + # Make sure the item id is a string + if isinstance(maybe_item_id, bytes): + item_id: str = maybe_item_id.decode('utf-8') + elif isinstance(maybe_item_id, str): + item_id: str = maybe_item_id + else: + raise Exception("item id from work queue not bytes or string") + + # If we got an item, fetch the associated data. + data: bytes | None = db.get(self._item_data_key.of(item_id)) + if data is None: + if block and timeout == 0: + continue + return None + + # Setup the lease item. + # NOTE: Racing for a lease is ok. + db.setex(self._lease_key.of(item_id), lease_secs, self._session) + return Item(data, id=item_id) def complete(self, db: Redis, item: Item) -> bool: """Marks a job as completed and remove it from the work queue. After `complete` has been @@ -143,17 +116,63 @@ def complete(self, db: Redis, item: Item) -> bool: `complete` returns a boolean indicating if *the job has been removed* **and** *this worker was the first worker to call `complete`*. So, while lease might give the same job to multiple workers, complete will return `true` for only one worker.""" - removed = int(db.lrem(self._processing_key, 0, item.id())) - # Only complete the work if it was still in the processing queue - if removed == 0: - return False item_id = item.id() - # TODO: The cleaner should also handle these... :( - db.pipeline() \ + item_del_result, _, _ = db.pipeline() \ .delete(self._item_data_key.of(item_id)) \ + .lrem(self._processing_key, 0, item.id()) \ .delete(self._lease_key.of(item_id)) \ .execute() - return True + return item_del_result is not None and item_del_result != 0 + + def _lease_exists(self, db: Redis, item_id: str) -> bool: + """True iff a lease on 'item_id' exists.""" + return db.exists(self._lease_key.of(item_id)) != 0 + + def light_clean(self, db: Redis) -> None: + # A light clean only checks items in the processing queue + processing: list[bytes | str] = db.lrange( + self._processing_key, 0, -1, + ) + for item_id in processing: + item_id = _value_str(item_id) + # If there's no lease for the item, then it should be reset. + if not self._lease_exists(db, item_id): + # We also check the item actually exists before pushing it back to the main queue + if db.exists(self._item_data_key.of(item_id)): + print(item_id, 'has no lease, it will be reset') + db.pipeline() \ + .lrem(self._processing_key, 0, item_id) \ + .lpush(self._main_queue_key, item_id) \ + .execute() + else: + print(item_id, 'was in the processing queue but does not exist') + db.lrem(self._processing_key, 0, item_id) + + def deep_clean(self, db: Redis) -> None: + # A deep clean checks all data keys + res: tuple[list[bytes | str], list[bytes | str]] = db.pipeline() \ + .keys(self._item_data_key.of('*')) \ + .lrange(self._main_queue_key, 0, -1) \ + .execute() + item_data_keys, main_queue = res + main_queue = list(map(_value_str, main_queue)) + for item_data_key in item_data_keys: + item_id = _value_str(item_data_key)[len(self._item_data_key.prefix):] + # If the item isn't in the queue, and there's no lease for the item, then it should be + # reset. + if item_id not in main_queue and not self._lease_exists(db, item_id): + print(item_id, 'has no lease, it will be reset') + db.pipeline() \ + .lrem(self._processing_key, 0, item_id) \ + .lpush(self._main_queue_key, item_id) \ + .execute() + + +def _value_str(value: bytes | str) -> str: + if isinstance(value, bytes): + return value.decode('utf-8') + assert isinstance(value, str) + return value __version__ = "0.1.2" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9e4ef0e..1c2582d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "redis-work-queue" -version = "0.1.6" +version = "0.3.0" edition = "2021" license = "MIT" description = "A work queue, on top of a redis database, with implementations in Python, Rust, Go, Node.js (TypeScript) and Dotnet (C#)." @@ -17,7 +17,7 @@ serde_json = "1" futures = "0.3" [dependencies.redis] -version = "0.23" +version = "0.26" features = ["aio", "async-std-comp", "connection-manager"] [dependencies.serde] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fa57529..3d6f915 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -261,15 +261,32 @@ impl WorkQueue { main_queue_key: name.of(":queue"), processing_key: name.of(":processing"), //cleaning_key: name.of(":cleaning"), - lease_key: name.and(":leased_by_session:"), + lease_key: name.and(":lease:"), item_data_key: name.and(":item:"), } } - /// Add an item to the work queue. This adds the redis commands onto the pipeline passed. + /// Add an item to the work queue. + /// + /// If an item with the same ID already exists, this item is not added, and `false` is returned. Otherwise, if the item is added `true` is returned. /// - /// Use [`WorkQueue::add_item`] if you don't want to pass a pipeline directly. - pub fn add_item_to_pipeline(&self, pipeline: &mut redis::Pipeline, item: &Item) { + /// If you know the item ID is unique, and not already in the queue, use the optimised + /// [`WorkQueue::add_unique_item`] instead. + pub async fn add_item(&self, db: &mut C, item: &Item) -> RedisResult { + let added = db + .set_nx(self.item_data_key.of(&item.id), item.data.as_ref()) + .await?; + if added { + db.lpush(&self.main_queue_key, &item.id).await?; + } + Ok(added) + } + + /// Add an item, which is known to have an ID not already in the queue, to the work queue. This + /// adds the redis commands onto the pipeline passed. + /// + /// Use [`WorkQueue::add_unique_item`] if you don't want to pass a pipeline directly. + pub fn add_unique_item_to_pipeline(&self, pipeline: &mut redis::Pipeline, item: &Item) { // Add the item data // NOTE: it's important that the data is added first, otherwise someone could pop the item // before the data is ready @@ -278,12 +295,16 @@ impl WorkQueue { pipeline.lpush(&self.main_queue_key, &item.id); } - /// Add an item to the work queue. + /// Add an item, which is known to have an ID not already in the queue, to the work queue. /// /// This creates a pipeline and executes it on the database. - pub async fn add_item(&self, db: &mut C, item: &Item) -> RedisResult<()> { + pub async fn add_unique_item( + &self, + db: &mut C, + item: &Item, + ) -> RedisResult<()> { let mut pipeline = Box::new(redis::pipe()); - self.add_item_to_pipeline(&mut pipeline, item); + self.add_unique_item_to_pipeline(&mut pipeline, item); pipeline.query_async(db).await } @@ -322,44 +343,48 @@ impl WorkQueue { timeout: Option, lease_duration: Duration, ) -> RedisResult> { - // First, to get an item, we try to move an item from the main queue to the processing list. - let item_id: Option = match timeout { - Some(Duration::ZERO) => { - db.rpoplpush(&self.main_queue_key, &self.processing_key) + loop { + // First, to get an item, we try to move an item from the main queue to the processing list. + let Some(item_id): Option = (match timeout { + Some(Duration::ZERO) => { + db.rpoplpush(&self.main_queue_key, &self.processing_key) + .await? + } + _ => { + db.brpoplpush( + &self.main_queue_key, + &self.processing_key, + timeout.map(|d| d.as_secs() as f64).unwrap_or(0.), + ) .await? - } - _ => { - db.brpoplpush( - &self.main_queue_key, - &self.processing_key, - timeout.map(|d| d.as_secs() as usize).unwrap_or(0), - ) - .await? - } - }; + } + }) else { + return Ok(None); + }; + + // If we got an item, fetch the associated data. + let item_data: Vec = match db.get(self.item_data_key.of(&item_id)).await? { + Some(item_data) => item_data, + // If the item doesn't actually exist, and there's no timeout, just try again. + None if timeout == None => continue, + // If there was a timeout, we return early. + None => return Ok(None), + }; + + // Now setup the lease item. + // NOTE: Racing for a lease is ok + db.set_ex( + self.lease_key.of(&item_id), + &self.session, + lease_duration.as_secs(), + ) + .await?; - // If we got an item, fetch the associated data. - let item = match item_id { - Some(item_id) => Item { - data: db - .get::<_, Vec>(self.item_data_key.of(&item_id)) - .await? - .into_boxed_slice(), + return Ok(Some(Item { + data: item_data.into_boxed_slice(), id: item_id, - }, - None => return Ok(None), - }; - - // Now setup the lease item. - // NOTE: Racing for a lease is ok - db.set_ex( - self.lease_key.of(&item.id), - &self.session, - lease_duration.as_secs() as usize, - ) - .await?; - - Ok(Some(item)) + })); + } } /// Marks a job as completed and remove it from the work queue. After `complete` has been called @@ -369,20 +394,17 @@ impl WorkQueue { /// was the first worker to call `complete`*. So, while lease might give the same job to /// multiple workers, complete will return `true` for only one worker. pub async fn complete(&self, db: &mut C, item: &Item) -> RedisResult { - let removed: usize = db.lrem(&self.processing_key, 0, &item.id).await?; - if removed == 0 { - return Ok(false); - } // If we did actually remove it, delete the item data and lease. // If we didn't really remove it, it's probably been returned to the work queue so the // data is still needed and the lease might not be ours (if it is still ours, it'll // expire anyway). - redis::pipe() + let (items_deleted, (), ()): (usize, (), ()) = redis::pipe() .del(self.item_data_key.of(&item.id)) + .lrem(&self.processing_key, 0, &item.id) .del(self.lease_key.of(&item.id)) .query_async(db) .await?; - Ok(true) + Ok(items_deleted > 0) } } diff --git a/scripts/clear-leases b/scripts/clear-leases index b696e9e..4e82370 100755 --- a/scripts/clear-leases +++ b/scripts/clear-leases @@ -15,4 +15,4 @@ if [ "$QUEUE" == "*" ]; then else echo Deleting all lease for queue: $QUEUE. fi -redis-cli --raw KEYS "$QUEUE:leased_by_session:*" | xargs redis-cli DEL +redis-cli --raw KEYS "$QUEUE:lease:*" | xargs redis-cli DEL diff --git a/tests/dotnet/RedisWorkQueueTests/Program.cs b/tests/dotnet/RedisWorkQueueTests/Program.cs index 5f170c9..c16b690 100644 --- a/tests/dotnet/RedisWorkQueueTests/Program.cs +++ b/tests/dotnet/RedisWorkQueueTests/Program.cs @@ -70,7 +70,7 @@ public static void Main(string[] args) Console.WriteLine("Result: ", jsonResult); if (sharedJobCounter % 12 == 0) - Thread.Sleep(1000*(sharedJobCounter % 4)); + Thread.Sleep(1000 * (sharedJobCounter % 4)); db.Set(sharedResultsKey.Of(job.ID), jsonResult); @@ -106,7 +106,7 @@ public static void Main(string[] args) Console.WriteLine($"Result: {result}"); if (dotNetJobCounter % 25 == 0) - Thread.Sleep(1000*(sharedJobCounter % 20)); + Thread.Sleep(1000 * (sharedJobCounter % 20)); db.Set(dotNetResultsKey.Of(job.ID), new byte[1] { (byte)result }); @@ -116,13 +116,20 @@ public static void Main(string[] args) if (dotNetQueue.Complete(db, job)) { + if (dotNetJobCounter % 6 == 0) + { + Console.WriteLine("Double completing"); + if (dotNetQueue.Complete(db, job)) + throw new Exception("double completion should have failed!"); + } + Console.WriteLine("Spawning shared jobs"); - sharedQueue.AddItem(db, Item.FromJson(new SharedJobData() + if (!sharedQueue.AddItem(db, Item.FromJson(new SharedJobData() { a = 19, b = result - })); - sharedQueue.AddItem(db, Item.FromJson(new SharedJobData() + }))) throw new Exception("item was not added"); + sharedQueue.AddUniqueItem(db, Item.FromJson(new SharedJobData() { a = 23, b = result diff --git a/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj b/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj index 4b8bdae..86e270f 100644 --- a/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj +++ b/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 enable enable exe - + diff --git a/tests/go/go.mod b/tests/go/go.mod index 5180e60..802fc25 100644 --- a/tests/go/go.mod +++ b/tests/go/go.mod @@ -5,12 +5,12 @@ replace github.com/mevitae/redis-work-queue/go => ../../go go 1.20 require ( - github.com/mevitae/redis-work-queue/go v0.1.0 - github.com/redis/go-redis/v9 v9.0.2 + github.com/mevitae/redis-work-queue/go v0.3.0 + github.com/redis/go-redis/v9 v9.6.1 ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect ) diff --git a/tests/go/go.sum b/tests/go/go.sum index 5362895..cb0d46b 100644 --- a/tests/go/go.sum +++ b/tests/go/go.sum @@ -1,14 +1,10 @@ -github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= -github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= -github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= diff --git a/tests/go/main.go b/tests/go/main.go index 7d0e621..d9cb936 100644 --- a/tests/go/main.go +++ b/tests/go/main.go @@ -144,6 +144,17 @@ func main() { panic(err) } if completed { + if goJobCounter%6 == 0 { + fmt.Println("Double completing") + doubleCompleted, err := goQueue.Complete(ctx, db, job) + if err != nil { + panic(err) + } + if doubleCompleted { + panic("double completino should have failed!") + } + } + fmt.Println("Spawning shared jobs") // If we succesfully completed the result, create two new shared jobs. item, err := workqueue.NewItemFromJSONData(SharedJobData{ @@ -153,10 +164,13 @@ func main() { if err != nil { panic(err) } - err = sharedQueue.AddItem(ctx, db, item) + added, err := sharedQueue.AddItem(ctx, db, item) if err != nil { panic(err) } + if !added { + panic("item was not added") + } item, err = workqueue.NewItemFromJSONData(SharedJobData{ A: 11, @@ -165,7 +179,7 @@ func main() { if err != nil { panic(err) } - err = sharedQueue.AddItem(ctx, db, item) + err = sharedQueue.AddUniqueItem(ctx, db, item) if err != nil { panic(err) } diff --git a/tests/node/index.ts b/tests/node/index.ts index 16207a2..0b57d66 100644 --- a/tests/node/index.ts +++ b/tests/node/index.ts @@ -82,7 +82,14 @@ async function main() { // Complete the job unless we're 'unlucky' and crash again if (nodeJobCounter % 29 !== 0) { if (await nodeQueue.complete(db, job)) { - await sharedQueue.addItem( + if (nodeJobCounter % 6 === 0) { + console.log("Double completing") + if (await nodeQueue.complete(db, job)) { + throw new Error("double completion should have failed!"); + } + } + + if (!await sharedQueue.addItem( db, new Item( JSON.stringify({ @@ -90,8 +97,8 @@ async function main() { b: result, }), ), - ); - await sharedQueue.addItem( + )) throw new Error("item was not added"); + await sharedQueue.addUniqueItem( db, new Item( JSON.stringify({ diff --git a/tests/python-tests.py b/tests/python-tests.py index 2c2f4cc..39eb11f 100644 --- a/tests/python-tests.py +++ b/tests/python-tests.py @@ -96,12 +96,18 @@ print("Completing") # If we succesfully completed the result, create two new shared jobs. if python_queue.complete(db, job): + if python_job_counter % 6 == 0: + print("Double completing") + if python_queue.complete(db, job): + raise Exception("double completion should have failed"); + print("Spawning shared jobs") - shared_queue.add_item(db, Item.from_json_data({ + if not shared_queue.add_item(db, Item.from_json_data({ 'a': 13, 'b': result, - })) - shared_queue.add_item(db, Item.from_json_data({ + })): + raise Exception("item was not added") + shared_queue.add_unique_item(db, Item.from_json_data({ 'a': 17, 'b': result, })) diff --git a/tests/rust/Cargo.lock b/tests/rust/Cargo.lock index d8358f6..9da5d53 100644 --- a/tests/rust/Cargo.lock +++ b/tests/rust/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "async-channel" @@ -126,9 +126,9 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -428,9 +428,9 @@ checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -527,6 +527,34 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.31.1" @@ -550,9 +578,9 @@ checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -610,18 +638,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -658,9 +686,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.23.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd6543a7bc6428396845f6854ccf3d1ae8823816592e2cbe74f20f50f209d02" +checksum = "e902a69d09078829137b4a5d9d082e0490393537badd7c91a3d69d14639e115f" dependencies = [ "arc-swap", "async-std", @@ -670,11 +698,12 @@ dependencies = [ "futures", "futures-util", "itoa", + "num-bigint", "percent-encoding", "pin-project-lite", "ryu", "sha1_smol", - "socket2 0.4.9", + "socket2 0.5.3", "tokio", "tokio-retry", "tokio-util", @@ -683,7 +712,7 @@ dependencies = [ [[package]] name = "redis-work-queue" -version = "0.1.6" +version = "0.3.0" dependencies = [ "futures", "redis", @@ -694,7 +723,7 @@ dependencies = [ [[package]] name = "rust-tests" -version = "0.1.0" +version = "0.3.0" dependencies = [ "futures-lite", "redis", @@ -797,9 +826,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -904,9 +933,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", diff --git a/tests/rust/Cargo.toml b/tests/rust/Cargo.toml index 2b3a604..c8744e2 100644 --- a/tests/rust/Cargo.toml +++ b/tests/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-tests" -version = "0.1.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -8,6 +8,6 @@ edition = "2021" [dependencies] redis-work-queue = { path = "../../rust" } futures-lite = "1" -redis = "0.23" +redis = "0.26" serde = "1" serde_json = "1" diff --git a/tests/rust/src/main.rs b/tests/rust/src/main.rs index 1472cc1..6b7c8da 100644 --- a/tests/rust/src/main.rs +++ b/tests/rust/src/main.rs @@ -30,7 +30,7 @@ async fn async_main() -> RedisResult<()> { .next() .expect("first command line argument must be redis host"); let db = &mut redis::Client::open(format!("redis://{host}/"))? - .get_async_connection() + .get_multiplexed_async_connection() .await?; let rust_results_key = KeyPrefix::new("results:rust:".to_string()); @@ -49,17 +49,18 @@ async fn async_main() -> RedisResult<()> { shared_job_counter += 1; // First, try to get a job from the shared job queue - let timeout = if shared_job_counter%5 == 0 { + let timeout = if shared_job_counter % 5 == 0 { Some(Duration::from_secs(1)) } else { Some(Duration::ZERO) }; println!("Leasing from shared with timeout: {:?}", timeout); - let Some(job) = shared_queue.lease( - db, - timeout, - Duration::from_secs(2), - ).await? else { continue }; + let Some(job) = shared_queue + .lease(db, timeout, Duration::from_secs(2)) + .await? + else { + continue; + }; // Also, if we get 'unlucky', crash while completing the job. if shared_job_counter % 7 == 0 { println!("Dropping job"); @@ -97,17 +98,18 @@ async fn async_main() -> RedisResult<()> { rust_job_counter += 1; // First, try to get a job from the rust job queue - let timeout = if shared_job_counter%6 == 0 { + let timeout = if shared_job_counter % 6 == 0 { Some(Duration::from_secs(2)) } else { Some(Duration::ZERO) }; println!("Leasing from rust with timeout: {:?}", timeout); - let Some(job) = rust_queue.lease( - db, - timeout, - Duration::from_secs(1), - ).await? else { continue }; + let Some(job) = rust_queue + .lease(db, timeout, Duration::from_secs(1)) + .await? + else { + continue; + }; // Also, if we get 'unlucky', crash while completing the job. if rust_job_counter % 7 == 0 { println!("Dropping job"); @@ -132,6 +134,13 @@ async fn async_main() -> RedisResult<()> { if rust_job_counter % 29 != 0 { println!("Completing"); if rust_queue.complete(db, &job).await? { + if rust_job_counter % 6 == 0 { + println!("Double completing"); + if rust_queue.complete(db, &job).await? { + panic!("double completion should have failed!"); + } + } + println!("Spawning shared jobs"); // If we succesfully completed the result, create two new shared jobs. let item = Item::from_json_data(&SharedJobData { @@ -139,14 +148,16 @@ async fn async_main() -> RedisResult<()> { b: job.data[0] as i32, }) .unwrap(); - shared_queue.add_item(db, &item).await?; + if !shared_queue.add_item(db, &item).await? { + panic!("item was not added"); + } let item = Item::from_json_data(&SharedJobData { a: 5, b: job.data[0] as i32, }) .unwrap(); - shared_queue.add_item(db, &item).await?; + shared_queue.add_unique_item(db, &item).await?; } } else { println!("Dropping"); From fa949204041247bbcb74f70346eaeea032408d20 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Fri, 15 Mar 2024 21:48:19 +0000 Subject: [PATCH 02/17] dotnet: WorkQueue.LightClean: add tests --- tests/README.md | 13 +++- tests/dotnet-cleaner/Cleaner/Cleaner.csproj | 18 +++++ tests/dotnet-cleaner/Cleaner/Program.cs | 18 +++++ tests/dotnet-cleaner/run.sh | 3 + .../RedisWorkQueueTests.csproj | 4 +- tests/job-spawner-and-cleaner.py | 77 ++++++++++++++++--- tests/run-test.sh | 13 +++- 7 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 tests/dotnet-cleaner/Cleaner/Cleaner.csproj create mode 100644 tests/dotnet-cleaner/Cleaner/Program.cs create mode 100755 tests/dotnet-cleaner/run.sh diff --git a/tests/README.md b/tests/README.md index c0c192a..54d1113 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,6 +18,12 @@ From the `tests` directory, to run the integration tests with all languages, use ./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs ``` +To do the same, but wit the DotNet implementation of the cleaner, use: + +```bash +./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs -c ./dotnet-cleaner/run.sh +``` + For a summary of other options, run: ```bash @@ -38,7 +44,7 @@ For example: ##### --host -This can be used to set a specific redis server, the default is `localhost`. +This can be used to set a specific redis server, the default is `localhost:6379`. For example: @@ -46,6 +52,11 @@ For example: ./run-test.sh --tests "go_jobs,python_jobs" --host example.server.net:port ``` +##### --cleaner + +This sets a custom binary to be used to clean the work queues, see the docs in +[job-spawner-and-cleaner.py](./job-spawner-and-cleaner.py). + ## Unit tests also exist Each client implementation contains some unit tests. These are located within the implementations diff --git a/tests/dotnet-cleaner/Cleaner/Cleaner.csproj b/tests/dotnet-cleaner/Cleaner/Cleaner.csproj new file mode 100644 index 0000000..3e03b2b --- /dev/null +++ b/tests/dotnet-cleaner/Cleaner/Cleaner.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + diff --git a/tests/dotnet-cleaner/Cleaner/Program.cs b/tests/dotnet-cleaner/Cleaner/Program.cs new file mode 100644 index 0000000..8328831 --- /dev/null +++ b/tests/dotnet-cleaner/Cleaner/Program.cs @@ -0,0 +1,18 @@ +using RedisWorkQueue; +using FreeRedis; + +class Program +{ + public static void Main(string[] args) + { + RedisClient db = new RedisClient(args[0]); + for (; ; ) + { + string? queueName = Console.ReadLine(); + if (queueName == null) break; + if (queueName == "") throw new Exception("input line must be non-empty"); + new WorkQueue(new KeyPrefix(queueName)).LightClean(db); + Console.WriteLine("cleaned " + queueName); + } + } +} diff --git a/tests/dotnet-cleaner/run.sh b/tests/dotnet-cleaner/run.sh new file mode 100755 index 0000000..7d45836 --- /dev/null +++ b/tests/dotnet-cleaner/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(realpath "$(dirname $0)")"/Cleaner +exec dotnet run -v quiet "$@" diff --git a/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj b/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj index 86e270f..9d870bc 100644 --- a/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj +++ b/tests/dotnet/RedisWorkQueueTests/RedisWorkQueueTests.csproj @@ -12,7 +12,7 @@ - - + + diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index a072393..14e7eec 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -1,5 +1,6 @@ import sys import json +import subprocess from time import sleep import redis @@ -11,18 +12,76 @@ if len(sys.argv) < 2: raise Exception("first command line argument must be redis host") host = sys.argv[1].split(":") -queue_list_names = sys.argv[2].split(" ") +if len(sys.argv) < 3: + raise Exception("second command line argument must be space-separated list of queue names (don't ask me why an argument is space separated)") +queue_names = sys.argv[2].split(" ") db = redis.Redis(host=host[0], port=int(host[1]) if len(host) > 1 else 6379) if len(db.keys("*")) > 0: raise Exception("redis database isn't clean") -shared_queue = WorkQueue(KeyPrefix("shared_jobs")) +shared_queue_name = "shared_jobs" +shared_queue = WorkQueue(KeyPrefix(shared_queue_name)) queue_list = list(map( lambda name: WorkQueue(KeyPrefix(name)), - queue_list_names, + queue_names, )) +def python_light_clean(): + for queue in queue_list: + queue.light_clean(db) + shared_queue.light_clean(db) + +class ExternalCleaner: + """This wraps an external process to be used for queue cleaning. + + The process should read line from stdin and write lines to stdout. + + Upon reading a line, that line should be interpreted as a queue name, and that queue should be + cleaned. After the queue has been cleaned, the program should write to stdout "cleaned ", + followed by the name of the queue, followed by a newline. + + The program must process the cleaning requests in the order they are sent.""" + + def __init__(self, command: list[str]): + self.child: subprocess.Popen = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=sys.stderr, + text=True, + ) + + def clean(self, queue_name: str): + """Request and wait for a single queue to be cleaned.""" + if not self.check(): + raise Exception('cleaner process is dead') + + assert self.child.stdin is not None + self.child.stdin.write(queue_name + "\n") + self.child.stdin.flush() + + assert self.child.stdout is not None + output = "" + while not output.startswith("cleaned "): + output = self.child.stdout.readline().strip() + assert output == "cleaned " + queue_name + + def clean_all(self): + """Clean all the work queues.""" + for queue_name in queue_names: + self.clean(queue_name) + self.clean(shared_queue_name) + + def check(self) -> bool: + """Check that the process is still running.""" + return self.child.poll() is None + +light_clean = python_light_clean +if len(sys.argv) > 3 and sys.argv[3] != "": + # Pass the redis host to the cleaner command + light_clean = ExternalCleaner([sys.argv[3], sys.argv[1]]).clean_all + counter = 0 doom_counter = 0 revived = False @@ -58,9 +117,7 @@ else: # Otherwise, clean! print("Cleaning") - for queue in queue_list: - queue.light_clean(db) - shared_queue.light_clean(db) + light_clean() # The `doom_counter` counts the number of consecutive times all the lengths are 0. doom_counter = doom_counter + 1 if all(map( lambda queue: queue.queue_len(db) == 0 and queue.processing(db) == 0, @@ -98,15 +155,15 @@ } -for queue_name in queue_list_names[:]: +for queue_name in queue_names[:]: if queue_name not in expecting_dict_config: - queue_list_names.remove(queue_name) + queue_names.remove(queue_name) keys_to_delete = [] for queue_name in expecting_dict_config: - if queue_name not in queue_list_names: + if queue_name not in queue_names: keys_to_delete.append(queue_name) for queue_name in keys_to_delete: @@ -152,7 +209,7 @@ raise Exception('found unexpected key: ' + key) updated_names = [] -for name in queue_list_names: +for name in queue_names: updated_names.append(name.replace("_jobs", "")) total_count_keys = 0 diff --git a/tests/run-test.sh b/tests/run-test.sh index 142eb99..7a3d5d4 100755 --- a/tests/run-test.sh +++ b/tests/run-test.sh @@ -4,6 +4,7 @@ set -e # Default values tests="" host="localhost:6379" +cleaner="" display_usage() { @@ -11,6 +12,7 @@ display_usage() { echo "Options:" echo " -t, --tests Specify test categories (go_jobs, python_jobs, rust_jobs, node_jobs, dotnet_jobs). Example use './run-test.sh --tests "go_jobs,python_jobs"'" echo " -h, --host Set the host (default: localhost:6379)" + echo " -c, --cleaner Run as the cleaner binary, see the docs in job-spawner-and-cleaner.py" echo " -h, --help Display this help message" } @@ -30,6 +32,11 @@ while [[ $# -gt 0 ]]; do shift shift ;; + -c|--cleaner) + cleaner="$2" + shift + shift + ;; -h|--help) display_usage exit 0 @@ -93,8 +100,10 @@ if [[ "$tests" == *"dotnet"* ]]; then fi if [[ "$tests" == *"node"* ]]; then - cd node echo "Installing Node.js dependencies" + cd ../node + npm install + cd ../tests/node npm ci echo "Running Node.js workers..." npm run test "$host" > /tmp/redis-work-queue-test-logs/node-worker-1.txt & @@ -105,4 +114,4 @@ if [[ "$tests" == *"node"* ]]; then fi echo "Running spawner..." -python3 job-spawner-and-cleaner.py "$host" "$tests" +python3 job-spawner-and-cleaner.py "$host" "$tests" "$cleaner" From dbb4634b3ed5cb3dc96866568460ce45c3319268 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Fri, 15 Mar 2024 22:51:06 +0000 Subject: [PATCH 03/17] tests: ensure all cleaning cases are tested --- tests/job-spawner-and-cleaner.py | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index 14e7eec..1e196ee 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -64,6 +64,8 @@ def clean(self, queue_name: str): assert self.child.stdout is not None output = "" while not output.startswith("cleaned "): + if output != "": + print(output) output = self.child.stdout.readline().strip() assert output == "cleaned " + queue_name @@ -116,7 +118,43 @@ def check(self) -> bool: queue.add_item(db, Item(bytes([n]))) else: # Otherwise, clean! - print("Cleaning") + print("Cleaning " + str(counter % 13)) + # This creates all the possible messed-up cleaning cases: + if counter % 13 == 2 or counter % 13 == 3: + # Occasionally move items from processing -> cleaning + print(db.rpoplpush(KeyPrefix(shared_queue_name).of(":processing"), KeyPrefix(shared_queue_name).of(":cleaning"))) + elif counter % 13 == 4: + # Occasionally move items from queue -> cleaning + print(db.rpoplpush(KeyPrefix(shared_queue_name).of(":queue"), KeyPrefix(shared_queue_name).of(":cleaning"))) + elif counter % 13 == 5: + # Occasionally copy items from queue -> cleaning + items = db.lrange(KeyPrefix(shared_queue_name).of(":queue"), 0, 1) + print(items) + if len(items) > 0: + db.lpush(KeyPrefix(shared_queue_name).of(":cleaning"), items[0]) + elif counter % 13 == 6: + # Occasionally copy items from processing -> cleaning + items = db.lrange(KeyPrefix(shared_queue_name).of(":processing"), 0, 1) + print(items) + if len(items) > 0: + db.lpush(KeyPrefix(shared_queue_name).of(":cleaning"), items[0]) + elif counter % 13 == 7: + # Occasionally copy items from processing -> cleaning + items = db.lrange(KeyPrefix(shared_queue_name).of(":processing"), 0, 1) + print(items) + # And delete the lease... + if len(items) > 0: + item = str(items[0], 'utf-8') + db.delete(shared_queue._lease_key.of(item)) + db.lpush(shared_queue._cleaning_key, item) + elif counter % 13 == 8: + # Occasionally move items from processing -> cleaning + item = db.rpoplpush(shared_queue._processing_key, shared_queue._cleaning_key) + print(item) + # And delete the lease... + if item is not None: + item = str(item, 'utf-8') + db.delete(shared_queue._lease_key.of(item)) light_clean() # The `doom_counter` counts the number of consecutive times all the lengths are 0. doom_counter = doom_counter + 1 if all(map( From cb629c02c20e83ab7914a804b52cc813ef70f835 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Fri, 15 Mar 2024 23:39:11 +0000 Subject: [PATCH 04/17] tests: check workers receive a minimum number of shared jobs --- tests/job-spawner-and-cleaner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index 1e196ee..eb6b757 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -254,7 +254,8 @@ def check(self) -> bool: for key in shared_counts.keys(): total_count_keys += shared_counts[key] -maximum_allowed = total_count_keys/len(shared_counts)*1.2 +minimum_allowed = total_count_keys/len(shared_counts)*0.7 +maximum_allowed = total_count_keys/len(shared_counts)*1.3 print("Maximum number of job counts:", maximum_allowed) @@ -262,4 +263,5 @@ def check(self) -> bool: assert key in updated_names # Check that it's fairly well balanced print(key, "Job counts:", shared_counts[key]) + assert minimum_allowed < shared_counts[key] assert shared_counts[key] < maximum_allowed From 246fdf9402f26fbf105016a43570a190e377d267 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 05/17] Correct cleaning tests --- tests/job-spawner-and-cleaner.py | 42 +++++++++++--------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index eb6b757..5bd1430 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -32,6 +32,11 @@ def python_light_clean(): queue.light_clean(db) shared_queue.light_clean(db) +def python_deep_clean(): + for queue in queue_list: + queue.deep_clean(db) + shared_queue.deep_clean(db) + class ExternalCleaner: """This wraps an external process to be used for queue cleaning. @@ -80,6 +85,7 @@ def check(self) -> bool: return self.child.poll() is None light_clean = python_light_clean +deep_clean = python_deep_clean if len(sys.argv) > 3 and sys.argv[3] != "": # Pass the redis host to the cleaner command light_clean = ExternalCleaner([sys.argv[3], sys.argv[1]]).clean_all @@ -121,40 +127,20 @@ def check(self) -> bool: print("Cleaning " + str(counter % 13)) # This creates all the possible messed-up cleaning cases: if counter % 13 == 2 or counter % 13 == 3: - # Occasionally move items from processing -> cleaning - print(db.rpoplpush(KeyPrefix(shared_queue_name).of(":processing"), KeyPrefix(shared_queue_name).of(":cleaning"))) + # Occasionally remove items from processing + db.rpop(KeyPrefix(shared_queue_name).of(":processing")) elif counter % 13 == 4: - # Occasionally move items from queue -> cleaning - print(db.rpoplpush(KeyPrefix(shared_queue_name).of(":queue"), KeyPrefix(shared_queue_name).of(":cleaning"))) + # Occasionally remove items from queue + db.rpop(KeyPrefix(shared_queue_name).of(":queue")) elif counter % 13 == 5: - # Occasionally copy items from queue -> cleaning + # Occasionally copy items from queue -> processing items = db.lrange(KeyPrefix(shared_queue_name).of(":queue"), 0, 1) print(items) if len(items) > 0: - db.lpush(KeyPrefix(shared_queue_name).of(":cleaning"), items[0]) + db.lpush(KeyPrefix(shared_queue_name).of(":processing"), items[0]) elif counter % 13 == 6: - # Occasionally copy items from processing -> cleaning - items = db.lrange(KeyPrefix(shared_queue_name).of(":processing"), 0, 1) - print(items) - if len(items) > 0: - db.lpush(KeyPrefix(shared_queue_name).of(":cleaning"), items[0]) - elif counter % 13 == 7: - # Occasionally copy items from processing -> cleaning - items = db.lrange(KeyPrefix(shared_queue_name).of(":processing"), 0, 1) - print(items) - # And delete the lease... - if len(items) > 0: - item = str(items[0], 'utf-8') - db.delete(shared_queue._lease_key.of(item)) - db.lpush(shared_queue._cleaning_key, item) - elif counter % 13 == 8: - # Occasionally move items from processing -> cleaning - item = db.rpoplpush(shared_queue._processing_key, shared_queue._cleaning_key) - print(item) - # And delete the lease... - if item is not None: - item = str(item, 'utf-8') - db.delete(shared_queue._lease_key.of(item)) + # Occasionally deep clean + deep_clean() light_clean() # The `doom_counter` counts the number of consecutive times all the lengths are 0. doom_counter = doom_counter + 1 if all(map( From d64f81c0cbb5c1d5b7e44a256ca0b4ee2abd8333 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Fri, 27 Oct 2023 14:21:53 +0100 Subject: [PATCH 06/17] Add tests for Python implementation of add_new_item and publish pre-release --- tests/transaction-tests/check.py | 32 +++++ tests/transaction-tests/python-tests.py | 162 ++++++++++++++++++++++++ tests/transaction-tests/run.sh | 78 ++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 tests/transaction-tests/check.py create mode 100644 tests/transaction-tests/python-tests.py create mode 100755 tests/transaction-tests/run.sh diff --git a/tests/transaction-tests/check.py b/tests/transaction-tests/check.py new file mode 100644 index 0000000..68879ce --- /dev/null +++ b/tests/transaction-tests/check.py @@ -0,0 +1,32 @@ +import sys +import random +from time import sleep + +import redis +from redis import Redis, WatchError + +sys.path.insert(0, '../../python') +from redis_work_queue import KeyPrefix, Item, WorkQueue + + +if len(sys.argv) < 2: + raise Exception("first command line argument must be redis host") + +host = sys.argv[1].split(":") +db = redis.Redis(host=host[0], port=int(host[1]) if len(host) > 1 else 6379) + +python_results_key = KeyPrefix("results:python:") +shared_results_key = KeyPrefix("results:shared:") + +python_queue = WorkQueue(KeyPrefix("python_jobs")) +shared_queue = WorkQueue(KeyPrefix("shared_jobs")) + +def check_queue(queue: WorkQueue): + """Ensure a queue has exatly 200 items, with ID 0, 1, 2, ..., 199.""" + remaining = list(range(0, 200)) + for item in db.lrange(queue._main_queue_key, 0, -1): + remaining.remove(int(item)) + assert len(remaining) == 0 + +check_queue(python_queue) +check_queue(shared_queue) diff --git a/tests/transaction-tests/python-tests.py b/tests/transaction-tests/python-tests.py new file mode 100644 index 0000000..006f9d6 --- /dev/null +++ b/tests/transaction-tests/python-tests.py @@ -0,0 +1,162 @@ +from abc import ABC, abstractmethod +import sys +import random +from time import sleep + +import redis +from redis import Redis, WatchError + +sys.path.insert(0, '../../python') +from redis_work_queue import KeyPrefix, Item, WorkQueue + + +if len(sys.argv) < 2: + raise Exception("first command line argument must be redis host") + +host = sys.argv[1].split(":") +db = redis.Redis(host=host[0], port=int(host[1]) if len(host) > 1 else 6379) + + +class ItemAdder(ABC): + """An abstract class containing the `add_item` method to add an item to a work queue. + + After running some tests, the `check` method should be called to ensure that all cases within `add_item` actually occurred. +Before running another set of tests, `reset` should be called.""" + @abstractmethod + def add_item(self, queue: WorkQueue, db: Redis, item: Item): + ... + + @abstractmethod + def check(self) -> bool: + ... + + @abstractmethod + def reset(self): + ... + + +class AddItem(ItemAdder): + """An ItemAdder using the default `WorkQueue.add_item` method, with no checks.""" + + def __init__(self): + pass + + def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: + queue.add_item(db, item) + return True + + def check(self) -> bool: + return True + + def reset(self): + pass + + +class AddNewItem(ItemAdder): + """An ItemAdder using the default `WorkQueue.add_new_item` method, which checks if, at some + point during the test, `add_new_item` has returned both `True` and `False`.""" + + def __init__(self): + self.reset() + + def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: + if queue.add_new_item(db, item): + self.seen_true = True + return True + else: + self.seen_false = True + return False + + def check(self) -> bool: + return self.seen_true and self.seen_false + + def reset(self): + self.seen_true = False + self.seen_false = False + + +class AddNewItemWithSleep(ItemAdder): + """An ItemAdder which uses a copy of `WorkQueue.add_new_item`, except it sleeps in the middle of + the transaction, and checks that this caused the transaction to fail at least once.""" + + def __init__(self): + self.reset() + + def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: + while True: + try: + pipeline = db.pipeline(transaction=True) + pipeline.watch(queue._main_queue_key, queue._processing_key) + + if ( + pipeline.lpos(queue._main_queue_key, item.id()) is not None + or pipeline.lpos(queue._processing_key, item.id()) is not None + ): + self.seen_unwatch = True + pipeline.unwatch() + return False + + sleep(random.randint(1, 8)/20) + pipeline.multi() + queue.add_item_to_pipeline(pipeline, item) + pipeline.execute() + self.seen_tx_succeed = True + return True + except WatchError: + self.seen_tx_fail = True + continue + + def check(self) -> bool: + return self.seen_tx_succeed and self.seen_tx_fail and self.seen_unwatch + + def reset(self): + self.seen_tx_fail = False + self.seen_tx_succeed = False + self.seen_unwatch = False + + +# Decide on the adder implementation to use, from the command line arguments. +if len(sys.argv) > 2 and sys.argv[2] == "--add-item": + adder = AddItem() +elif len(sys.argv) > 2 and sys.argv[2] == "--add-new-item": + adder = AddNewItem() +elif len(sys.argv) > 2 and sys.argv[2] == "--add-new-item-with-sleep": + adder = AddNewItemWithSleep() +else: + raise Exception( + "second argument should be `--add-item`, `--add-new-item` or `--add-new-item-with-sleep`" + ) + +python_queue = WorkQueue(KeyPrefix("python_jobs")) +shared_queue = WorkQueue(KeyPrefix("shared_jobs")) + +# Add 100 unique jobs to the python queue: +for idx in range(100, 200): + id = str(idx) + if adder.add_item(python_queue, db, Item(str(idx), id)) and idx == 150: + # If we're ahead at item 150, sleep so we end up behind + sleep(10) + +assert adder.check() +adder.reset() + +# Add 100 unique jobs to the shared queue: +for idx in range(100, 200): + id = str(idx) + if adder.add_item(shared_queue, db, Item(str(idx), id)) and idx == 150: + # If we're ahead at item 150, sleep so we end up behind (this sleep is intentionally longer + # than the last one) + sleep(20) + +assert adder.check() +adder.reset() + +# Add 100 jobs, each 10 times, to both queues: +for idx in range(0, 1000): + id = str(idx//10) + if adder.add_item(python_queue, db, Item(str(idx), id)) and idx == 500: + sleep(30) + adder.add_item(shared_queue, db, Item(str(idx), id)) + +assert adder.check() +adder.reset() diff --git a/tests/transaction-tests/run.sh b/tests/transaction-tests/run.sh new file mode 100755 index 0000000..e223fc6 --- /dev/null +++ b/tests/transaction-tests/run.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -e + +echo --- Single Threaded Tests --- + +echo ----------- Python ---------- +echo ---------- add_item --------- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +# No checks, so this must pass +python3 ./python-tests.py localhost --add-item +# There will be duplicates, so check should fail +not python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID + +echo -------- add_new_item ------- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +# There won't be duplicate items from other workers, so this will fail. +not python3 ./python-tests.py localhost --add-new-item +# The previous worker didn't complete, so the queue shouldn't contain everything. +not python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID + +echo -- add_new_item_with_sleep -- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +# There won't be duplicate items from other workers, so this will fail. +not python3 ./python-tests.py localhost --add-new-item-with-sleep +# The previous worker didn't complete, so the queue shouldn't contain everything. +not python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID + +echo ---- Duel Threaded Tests ---- + +echo ----------- Python ---------- +echo ---------- add_item --------- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +# No checks, so this should succeed. +python3 ./python-tests.py localhost --add-item & +FIRST_THREAD_PID=$! +# No checks, so this should succeed. +python3 ./python-tests.py localhost --add-item +wait $FIRST_THREAD_PID +# There will be duplicates, so check should fail +not python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID +echo -------- add_new_item ------- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +# There will be jobs from the other worker, so this should pass checks. +python3 ./python-tests.py localhost --add-new-item & +FIRST_THREAD_PID=$! +# There will be jobs from the other worker, so this should pass checks. +python3 ./python-tests.py localhost --add-new-item +wait $FIRST_THREAD_PID +# And there should be no duplicate items! +python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID +echo -- add_new_item_with_sleep -- +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$ +# There will be jobs from the other worker, so this should pass checks. +python3 ./python-tests.py localhost --add-new-item-with-sleep & +FIRST_THREAD_PID=$! +# There will be jobs from the other worker, so this should pass checks. +python3 ./python-tests.py localhost --add-new-item-with-sleep +wait $FIRST_THREAD_PID +# And there should be no duplicate items! +python3 ./check.py localhost +kill $REDIS_PID +wait $REDIS_PID From 2a59c454b6df565123846e63d9960271c9d9c993 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 07/17] Update transaction tests to test new adding methods (still Python only) --- .../check.py | 0 .../python-tests.py | 65 ++++--------------- .../run.sh | 53 +++++---------- 3 files changed, 28 insertions(+), 90 deletions(-) rename tests/{transaction-tests => add-item-tests}/check.py (100%) rename tests/{transaction-tests => add-item-tests}/python-tests.py (59%) rename tests/{transaction-tests => add-item-tests}/run.sh (51%) diff --git a/tests/transaction-tests/check.py b/tests/add-item-tests/check.py similarity index 100% rename from tests/transaction-tests/check.py rename to tests/add-item-tests/check.py diff --git a/tests/transaction-tests/python-tests.py b/tests/add-item-tests/python-tests.py similarity index 59% rename from tests/transaction-tests/python-tests.py rename to tests/add-item-tests/python-tests.py index 006f9d6..fc5d3d5 100644 --- a/tests/transaction-tests/python-tests.py +++ b/tests/add-item-tests/python-tests.py @@ -23,7 +23,7 @@ class ItemAdder(ABC): After running some tests, the `check` method should be called to ensure that all cases within `add_item` actually occurred. Before running another set of tests, `reset` should be called.""" @abstractmethod - def add_item(self, queue: WorkQueue, db: Redis, item: Item): + def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: ... @abstractmethod @@ -35,14 +35,14 @@ def reset(self): ... -class AddItem(ItemAdder): - """An ItemAdder using the default `WorkQueue.add_item` method, with no checks.""" +class AddUniqueItem(ItemAdder): + """An ItemAdder using the `WorkQueue.add_unique_item` method, with no checks.""" def __init__(self): pass def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: - queue.add_item(db, item) + queue.add_unique_item(db, item) return True def check(self) -> bool: @@ -52,15 +52,16 @@ def reset(self): pass -class AddNewItem(ItemAdder): - """An ItemAdder using the default `WorkQueue.add_new_item` method, which checks if, at some - point during the test, `add_new_item` has returned both `True` and `False`.""" +class AddItem(ItemAdder): + """An ItemAdder using the default `WorkQueue.add_item` method, which checks + if, at some point during the test, `add_new_item` has returned both `True` + and `False`.""" def __init__(self): self.reset() def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: - if queue.add_new_item(db, item): + if queue.add_item(db, item): self.seen_true = True return True else: @@ -75,56 +76,14 @@ def reset(self): self.seen_false = False -class AddNewItemWithSleep(ItemAdder): - """An ItemAdder which uses a copy of `WorkQueue.add_new_item`, except it sleeps in the middle of - the transaction, and checks that this caused the transaction to fail at least once.""" - - def __init__(self): - self.reset() - - def add_item(self, queue: WorkQueue, db: Redis, item: Item) -> bool: - while True: - try: - pipeline = db.pipeline(transaction=True) - pipeline.watch(queue._main_queue_key, queue._processing_key) - - if ( - pipeline.lpos(queue._main_queue_key, item.id()) is not None - or pipeline.lpos(queue._processing_key, item.id()) is not None - ): - self.seen_unwatch = True - pipeline.unwatch() - return False - - sleep(random.randint(1, 8)/20) - pipeline.multi() - queue.add_item_to_pipeline(pipeline, item) - pipeline.execute() - self.seen_tx_succeed = True - return True - except WatchError: - self.seen_tx_fail = True - continue - - def check(self) -> bool: - return self.seen_tx_succeed and self.seen_tx_fail and self.seen_unwatch - - def reset(self): - self.seen_tx_fail = False - self.seen_tx_succeed = False - self.seen_unwatch = False - - # Decide on the adder implementation to use, from the command line arguments. if len(sys.argv) > 2 and sys.argv[2] == "--add-item": adder = AddItem() -elif len(sys.argv) > 2 and sys.argv[2] == "--add-new-item": - adder = AddNewItem() -elif len(sys.argv) > 2 and sys.argv[2] == "--add-new-item-with-sleep": - adder = AddNewItemWithSleep() +elif len(sys.argv) > 2 and sys.argv[2] == "--add-unique-item": + adder = AddUniqueItem() else: raise Exception( - "second argument should be `--add-item`, `--add-new-item` or `--add-new-item-with-sleep`" + "second argument should be `--add-item` or `--add-unique-item`" ) python_queue = WorkQueue(KeyPrefix("python_jobs")) diff --git a/tests/transaction-tests/run.sh b/tests/add-item-tests/run.sh similarity index 51% rename from tests/transaction-tests/run.sh rename to tests/add-item-tests/run.sh index e223fc6..7aaf460 100755 --- a/tests/transaction-tests/run.sh +++ b/tests/add-item-tests/run.sh @@ -7,70 +7,49 @@ echo ----------- Python ---------- echo ---------- add_item --------- echo -e 'save ""\nappendonly no' | redis-server - & REDIS_PID=$! -# No checks, so this must pass -python3 ./python-tests.py localhost --add-item -# There will be duplicates, so check should fail -not python3 ./check.py localhost -kill $REDIS_PID -wait $REDIS_PID - -echo -------- add_new_item ------- -echo -e 'save ""\nappendonly no' | redis-server - & -REDIS_PID=$! # There won't be duplicate items from other workers, so this will fail. -not python3 ./python-tests.py localhost --add-new-item +! python3 ./python-tests.py localhost --add-item # The previous worker didn't complete, so the queue shouldn't contain everything. -not python3 ./check.py localhost +! python3 ./check.py localhost kill $REDIS_PID wait $REDIS_PID -echo -- add_new_item_with_sleep -- +echo ------ add_unique_item ------ echo -e 'save ""\nappendonly no' | redis-server - & REDIS_PID=$! -# There won't be duplicate items from other workers, so this will fail. -not python3 ./python-tests.py localhost --add-new-item-with-sleep -# The previous worker didn't complete, so the queue shouldn't contain everything. -not python3 ./check.py localhost +# No checks, so this must pass +python3 ./python-tests.py localhost --add-unique-item +# There will be duplicates, so check should fail +! python3 ./check.py localhost kill $REDIS_PID wait $REDIS_PID + echo ---- Duel Threaded Tests ---- echo ----------- Python ---------- -echo ---------- add_item --------- +echo ------ add_unique_item ------ echo -e 'save ""\nappendonly no' | redis-server - & REDIS_PID=$! # No checks, so this should succeed. -python3 ./python-tests.py localhost --add-item & +python3 ./python-tests.py localhost --add-unique-item & FIRST_THREAD_PID=$! # No checks, so this should succeed. -python3 ./python-tests.py localhost --add-item +python3 ./python-tests.py localhost --add-unique-item wait $FIRST_THREAD_PID # There will be duplicates, so check should fail -not python3 ./check.py localhost +! python3 ./check.py localhost kill $REDIS_PID wait $REDIS_PID -echo -------- add_new_item ------- + +echo ---------- add_item --------- echo -e 'save ""\nappendonly no' | redis-server - & REDIS_PID=$! # There will be jobs from the other worker, so this should pass checks. -python3 ./python-tests.py localhost --add-new-item & -FIRST_THREAD_PID=$! -# There will be jobs from the other worker, so this should pass checks. -python3 ./python-tests.py localhost --add-new-item -wait $FIRST_THREAD_PID -# And there should be no duplicate items! -python3 ./check.py localhost -kill $REDIS_PID -wait $REDIS_PID -echo -- add_new_item_with_sleep -- -echo -e 'save ""\nappendonly no' | redis-server - & -REDIS_PID=$ -# There will be jobs from the other worker, so this should pass checks. -python3 ./python-tests.py localhost --add-new-item-with-sleep & +python3 ./python-tests.py localhost --add-item & FIRST_THREAD_PID=$! # There will be jobs from the other worker, so this should pass checks. -python3 ./python-tests.py localhost --add-new-item-with-sleep +python3 ./python-tests.py localhost --add-item wait $FIRST_THREAD_PID # And there should be no duplicate items! python3 ./check.py localhost From d88210e299e973ce2efb3452ebb1535e18688400 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 08/17] tests: use external cleaner deep cleans, too --- tests/dotnet-cleaner/Cleaner/Cleaner.csproj | 4 +-- tests/dotnet-cleaner/Cleaner/Program.cs | 25 +++++++++++++---- tests/job-spawner-and-cleaner.py | 30 ++++++++++++++------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/tests/dotnet-cleaner/Cleaner/Cleaner.csproj b/tests/dotnet-cleaner/Cleaner/Cleaner.csproj index 3e03b2b..8551d8b 100644 --- a/tests/dotnet-cleaner/Cleaner/Cleaner.csproj +++ b/tests/dotnet-cleaner/Cleaner/Cleaner.csproj @@ -2,13 +2,13 @@ Exe - net7.0 + net8.0 enable enable - + diff --git a/tests/dotnet-cleaner/Cleaner/Program.cs b/tests/dotnet-cleaner/Cleaner/Program.cs index 8328831..a245261 100644 --- a/tests/dotnet-cleaner/Cleaner/Program.cs +++ b/tests/dotnet-cleaner/Cleaner/Program.cs @@ -8,11 +8,26 @@ public static void Main(string[] args) RedisClient db = new RedisClient(args[0]); for (; ; ) { - string? queueName = Console.ReadLine(); - if (queueName == null) break; - if (queueName == "") throw new Exception("input line must be non-empty"); - new WorkQueue(new KeyPrefix(queueName)).LightClean(db); - Console.WriteLine("cleaned " + queueName); + string? instruction = Console.ReadLine(); + if (instruction == null) break; + if (instruction == "") throw new Exception("input line must be non-empty"); + string[] parts = instruction.Split(':', 2); + if (parts.Length != 2) throw new Exception("input line must be of the format light:queue-name or deep:queue-name"); + string queueName = parts[1]; + var queue = new WorkQueue(new KeyPrefix(queueName)); + switch (parts[0]) + { + case "light": + queue.LightClean(db); + Console.WriteLine("light cleaned " + queueName); + break; + case "deep": + queue.DeepClean(db); + Console.WriteLine("deep cleaned " + queueName); + break; + default: + throw new Exception($"invalid cleaning mode: ${parts[0]}, should be \"light\" or \"deep\""); + } } } } diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index 5bd1430..7447464 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -57,28 +57,36 @@ def __init__(self, command: list[str]): text=True, ) - def clean(self, queue_name: str): - """Request and wait for a single queue to be cleaned.""" + def clean(self, mode: str, queue_name: str): + """Request and wait for a single queue to be cleaned (mode should be "light" or "deep").""" if not self.check(): raise Exception('cleaner process is dead') assert self.child.stdin is not None - self.child.stdin.write(queue_name + "\n") + self.child.stdin.write(mode + ":" +queue_name + "\n") self.child.stdin.flush() assert self.child.stdout is not None output = "" - while not output.startswith("cleaned "): + while not output.startswith(mode + " cleaned "): if output != "": print(output) output = self.child.stdout.readline().strip() - assert output == "cleaned " + queue_name + assert output == mode + " cleaned " + queue_name - def clean_all(self): - """Clean all the work queues.""" + def clean_all(self, mode: str): + """Clean all the work queues with the provided mode (either "light" or "deep").""" for queue_name in queue_names: - self.clean(queue_name) - self.clean(shared_queue_name) + self.clean(mode, queue_name) + self.clean(mode, shared_queue_name) + + def light_clean_all(self): + """Light clean all the work queues.""" + self.clean_all("light") + + def deep_clean_all(self): + """Light clean all the work queues.""" + self.clean_all("light") def check(self) -> bool: """Check that the process is still running.""" @@ -88,7 +96,9 @@ def check(self) -> bool: deep_clean = python_deep_clean if len(sys.argv) > 3 and sys.argv[3] != "": # Pass the redis host to the cleaner command - light_clean = ExternalCleaner([sys.argv[3], sys.argv[1]]).clean_all + cleaner = ExternalCleaner([sys.argv[3], sys.argv[1]]) + light_clean = cleaner.light_clean_all + deep_clean = cleaner.deep_clean_all counter = 0 doom_counter = 0 From e95b2cab47522b90918321f269a47ccd5c9e2523 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 09/17] Add Go cleaner tests --- tests/README.md | 6 ++++++ tests/go-cleaner/go.mod | 16 +++++++++++++++ tests/go-cleaner/go.sum | 10 ++++++++++ tests/go-cleaner/main.go | 43 ++++++++++++++++++++++++++++++++++++++++ tests/go-cleaner/run.sh | 3 +++ 5 files changed, 78 insertions(+) create mode 100644 tests/go-cleaner/go.mod create mode 100644 tests/go-cleaner/go.sum create mode 100644 tests/go-cleaner/main.go create mode 100755 tests/go-cleaner/run.sh diff --git a/tests/README.md b/tests/README.md index 54d1113..eed7206 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,6 +24,12 @@ To do the same, but wit the DotNet implementation of the cleaner, use: ./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs -c ./dotnet-cleaner/run.sh ``` +To do the same, but wit the Go implementation of the cleaner, use: + +```bash +./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs -c ./go-cleaner/run.sh +``` + For a summary of other options, run: ```bash diff --git a/tests/go-cleaner/go.mod b/tests/go-cleaner/go.mod new file mode 100644 index 0000000..3da9955 --- /dev/null +++ b/tests/go-cleaner/go.mod @@ -0,0 +1,16 @@ +module github.com/mevitae/redis-work-queue/tests/go-cleaner + +replace github.com/mevitae/redis-work-queue/go => ../../go + +go 1.20 + +require ( + github.com/mevitae/redis-work-queue/go v0.3.0 + github.com/redis/go-redis/v9 v9.6.1 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/google/uuid v1.6.0 // indirect +) diff --git a/tests/go-cleaner/go.sum b/tests/go-cleaner/go.sum new file mode 100644 index 0000000..cb0d46b --- /dev/null +++ b/tests/go-cleaner/go.sum @@ -0,0 +1,10 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= diff --git a/tests/go-cleaner/main.go b/tests/go-cleaner/main.go new file mode 100644 index 0000000..00f80d5 --- /dev/null +++ b/tests/go-cleaner/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + workqueue "github.com/mevitae/redis-work-queue/go" + "github.com/redis/go-redis/v9" +) + +func main() { + if len(os.Args) < 2 { + panic("first command line argument must be redis host") + } + + db := redis.NewClient(&redis.Options{ + Addr: os.Args[1], + }) + ctx := context.Background() + + stdin := bufio.NewReader(os.Stdin) + for { + instruction, err := stdin.ReadString('\n') + if err != nil { + panic(err) + } + instruction = instruction[:len(instruction)-1] + if queueName, ok := strings.CutPrefix(instruction, "light:"); ok { + queue := workqueue.NewWorkQueue(workqueue.KeyPrefix(queueName)) + queue.LightClean(ctx, db) + fmt.Println("light cleaned", queueName) + } else if queueName, ok := strings.CutPrefix(instruction, "deep:"); ok { + queue := workqueue.NewWorkQueue(workqueue.KeyPrefix(queueName)) + queue.DeepClean(ctx, db) + fmt.Println("deep cleaned", queueName) + } else { + panic("invalid cleaning mode") + } + } +} diff --git a/tests/go-cleaner/run.sh b/tests/go-cleaner/run.sh new file mode 100755 index 0000000..f799610 --- /dev/null +++ b/tests/go-cleaner/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(realpath "$(dirname $0)")" +exec go run . "$@" From a10334ff2c690aa211c8e0fb6dcbf62018327687 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Mon, 26 Aug 2024 23:55:42 +0100 Subject: [PATCH 10/17] Add `all-tests.sh` script --- tests/all-tests.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 tests/all-tests.sh diff --git a/tests/all-tests.sh b/tests/all-tests.sh new file mode 100755 index 0000000..7e699b6 --- /dev/null +++ b/tests/all-tests.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs +kill $REDIS_PID +wait $REDIS_PID + +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs -c ./go-cleaner/run.sh +kill $REDIS_PID +wait $REDIS_PID + +echo -e 'save ""\nappendonly no' | redis-server - & +REDIS_PID=$! +./run-test.sh -t go_jobs,python_jobs,rust_jobs,node_jobs,dotnet_jobs -c ./dotnet-cleaner/run.sh +kill $REDIS_PID +wait $REDIS_PID + +cd ./add-item-tests +./run.sh From 482ce675ed547e1c79227b53b0c608e17a388f96 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 01:49:33 +0100 Subject: [PATCH 11/17] go: correct lease checking logic, now passes tests --- go/WorkQueue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/WorkQueue.go b/go/WorkQueue.go index 76765d1..c60b1e1 100644 --- a/go/WorkQueue.go +++ b/go/WorkQueue.go @@ -207,7 +207,7 @@ func (workQueue *WorkQueue) Lease( } func (workQueue *WorkQueue) leaseExists(ctx context.Context, db *redis.Client, itemId string) (bool, error) { - exists, err := db.Exists(ctx, workQueue.itemDataKey.Of(itemId)).Result() + exists, err := db.Exists(ctx, workQueue.leaseKey.Of(itemId)).Result() return exists > 0, err } From 199aa08465bc49a5ceb18b01145dfd8e0576498e Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 01:55:29 +0100 Subject: [PATCH 12/17] DotNet: remove unused line --- dotnet/RedisWorkQueue/WorkQueue.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/RedisWorkQueue/WorkQueue.cs b/dotnet/RedisWorkQueue/WorkQueue.cs index d900d4c..34df334 100644 --- a/dotnet/RedisWorkQueue/WorkQueue.cs +++ b/dotnet/RedisWorkQueue/WorkQueue.cs @@ -224,7 +224,6 @@ public void DeepClean(IRedisClient db) itemDataKeys = (string[])results[0]; mainQueue = (string[])results[1]; } - var processing = db.LRange(ProcessingKey, 0, -1); foreach (string itemDataKey in itemDataKeys) { string itemId = itemDataKey.Substring(ItemDataKey.Prefix.Length); From 42f9830c856e41cb43b3a89b6714decdf9aebb4c Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 01:56:06 +0100 Subject: [PATCH 13/17] tests: go-cleaner: check for errors --- tests/go-cleaner/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/go-cleaner/main.go b/tests/go-cleaner/main.go index 00f80d5..384e5e4 100644 --- a/tests/go-cleaner/main.go +++ b/tests/go-cleaner/main.go @@ -30,14 +30,17 @@ func main() { instruction = instruction[:len(instruction)-1] if queueName, ok := strings.CutPrefix(instruction, "light:"); ok { queue := workqueue.NewWorkQueue(workqueue.KeyPrefix(queueName)) - queue.LightClean(ctx, db) + err = queue.LightClean(ctx, db) fmt.Println("light cleaned", queueName) } else if queueName, ok := strings.CutPrefix(instruction, "deep:"); ok { queue := workqueue.NewWorkQueue(workqueue.KeyPrefix(queueName)) - queue.DeepClean(ctx, db) + err = queue.DeepClean(ctx, db) fmt.Println("deep cleaned", queueName) } else { panic("invalid cleaning mode") } + if err != nil { + panic(err) + } } } From 61a3e4a92fdea48d8fa9e96f091921bd5c2365c3 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 01:56:20 +0100 Subject: [PATCH 14/17] tests: correctly call deep cleaner! --- tests/job-spawner-and-cleaner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/job-spawner-and-cleaner.py b/tests/job-spawner-and-cleaner.py index 7447464..dbc5822 100644 --- a/tests/job-spawner-and-cleaner.py +++ b/tests/job-spawner-and-cleaner.py @@ -86,7 +86,7 @@ def light_clean_all(self): def deep_clean_all(self): """Light clean all the work queues.""" - self.clean_all("light") + self.clean_all("deep") def check(self) -> bool: """Check that the process is still running.""" From dbcf0a74474628acf9790c07d31fec87b5bedb36 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 02:05:03 +0100 Subject: [PATCH 15/17] go: take the itemId... not the prefix... Oops! --- go/WorkQueue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/WorkQueue.go b/go/WorkQueue.go index c60b1e1..52947b6 100644 --- a/go/WorkQueue.go +++ b/go/WorkQueue.go @@ -261,7 +261,7 @@ func (workQueue *WorkQueue) DeepClean(ctx context.Context, db *redis.Client) err } mainQueue := mainQueueRes.Val() for _, itemDataKey := range itemDataKeys.Val() { - itemId := itemDataKey[:len(workQueue.itemDataKey)] + itemId := itemDataKey[len(workQueue.itemDataKey):] leaseExists, err := workQueue.leaseExists(ctx, db, itemId) if err != nil { return fmt.Errorf("failed to check if lease exists for %s: %w", itemId, err) From 5cc28c507e7b1f18e7b10ece76c9e9bdfc1a80b4 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 02:09:14 +0100 Subject: [PATCH 16/17] dotnet: correct log typos --- dotnet/RedisWorkQueue/WorkQueue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/RedisWorkQueue/WorkQueue.cs b/dotnet/RedisWorkQueue/WorkQueue.cs index 34df334..782e8fa 100644 --- a/dotnet/RedisWorkQueue/WorkQueue.cs +++ b/dotnet/RedisWorkQueue/WorkQueue.cs @@ -196,7 +196,7 @@ public void LightClean(IRedisClient db) // We also check the item actually exists before pushing it back to the main queue if (db.Exists(ItemDataKey.Of(itemId))) { - Console.WriteLine($"{itemId} has not lease, it will be reset"); + Console.WriteLine($"{itemId} has no lease, it will be reset"); using var pipe = db.StartPipe(); pipe.LRem(ProcessingKey, 0, itemId); pipe.LPush(MainQueueKey, itemId); @@ -231,7 +231,7 @@ public void DeepClean(IRedisClient db) // be reset. if (!mainQueue.Contains(itemId) && !LeaseExists(db, itemId)) { - Console.WriteLine($"{itemId} has not lease, it will be reset"); + Console.WriteLine($"{itemId} has no lease, it will be reset"); using var pipe = db.StartPipe(); pipe.LRem(ProcessingKey, 0, itemId); pipe.LPush(MainQueueKey, itemId); From 22b95e9e8cc37abf44ef47883b1ef523c3f6a309 Mon Sep 17 00:00:00 2001 From: Jacob O'Toole Date: Tue, 27 Aug 2024 02:13:53 +0100 Subject: [PATCH 17/17] go-cleaner: exit cleanly --- tests/go-cleaner/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/go-cleaner/main.go b/tests/go-cleaner/main.go index 384e5e4..0d2de02 100644 --- a/tests/go-cleaner/main.go +++ b/tests/go-cleaner/main.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "io" "os" "strings" @@ -25,6 +26,9 @@ func main() { for { instruction, err := stdin.ReadString('\n') if err != nil { + if err == io.EOF { + break + } panic(err) } instruction = instruction[:len(instruction)-1]