func qpeekgeneric( client *clients.Client, args []string, peekRight bool, ) ( interface{}, error, ) { queueName := args[0] unclaimedKey := db.UnclaimedKey(queueName) offset := "0" if peekRight { offset = "-1" } eventIDs, err := db.Inst.Cmd("LRANGE", unclaimedKey, offset, offset).List() if err != nil { return nil, fmt.Errorf("QPEEK* LRANGE: %s", err) } else if len(eventIDs) == 0 { return nil, nil } eventID := eventIDs[0] itemsKey := db.ItemsKey(queueName) var eventRaw string reply := db.Inst.Cmd("HGET", itemsKey, eventID) if reply.IsType(redis.Nil) { return nil, nil } if eventRaw, err = reply.Str(); err != nil { return nil, fmt.Errorf("QPEEK* HGET: %s", err) } return []string{eventID, eventRaw}, nil }
func qflush(client *clients.Client, args []string) (interface{}, error) { queue := args[0] unclaimedKey := db.UnclaimedKey(queue) claimedKey := db.ClaimedKey(queue) itemsKey := db.ItemsKey(queue) err := db.Inst.Cmd("DEL", unclaimedKey, claimedKey, itemsKey).Err if err != nil { return nil, fmt.Errorf("QFLUSH DEL: %s", err) } return okSS, nil }
func qstatus(client *clients.Client, args []string) (interface{}, error) { var queueNames []string if len(args) == 0 { queueNames = db.AllQueueNames() sort.Strings(queueNames) } else { queueNames = args } queueInfos := make([][]interface{}, 0, len(queueNames)) for i := range queueNames { queueName := queueNames[i] claimedCount := 0 availableCount := 0 totalCount := 0 consumerCount := int64(0) unclaimedKey := db.UnclaimedKey(queueName) claimedKey := db.ClaimedKey(queueName) claimedCount, err := db.Inst.Cmd("LLEN", claimedKey).Int() if err != nil { return nil, fmt.Errorf("QSTATUS LLEN claimed: %s", err) } availableCount, err = db.Inst.Cmd("LLEN", unclaimedKey).Int() if err != nil { return nil, fmt.Errorf("QSTATUS LLEN unclaimed: %s", err) } totalCount = availableCount + claimedCount consumerCount, err = consumers.QueueConsumerCount(queueName) if err != nil { return nil, fmt.Errorf("QSTATUS QueueConsumerCount: %s", err) } queueInfos = append(queueInfos, []interface{}{ queueName, int64(totalCount), int64(claimedCount), consumerCount, }) } return queueInfos, nil }
func qpushgeneric( _ *clients.Client, args []string, pushRight bool, ) ( interface{}, error, ) { queueName, eventID, contents := args[0], args[1], args[2] restArgs := args[3:] if len(restArgs) > 0 && isEqualUpper("NOBLOCK", restArgs[0]) { coreArgs := args[:3] select { case qpushBGCh <- &qpushBG{coreArgs, pushRight}: return okSS, nil default: return fmt.Errorf("ERR too busy to process NOBLOCK event"), nil } } itemsKey := db.ItemsKey(queueName) created, err := db.Inst.Cmd("HSETNX", itemsKey, eventID, contents).Int() if err != nil { return nil, fmt.Errorf("QPUSH* HSETNX: %s", err) } else if created == 0 { return fmt.Errorf("ERR duplicate event %s", eventID), nil } unclaimedKey := db.UnclaimedKey(queueName) cmd := "LPUSH" if pushRight { cmd = "RPUSH" } channelName := db.QueueChannelNameKey(queueName) _, err = db.Inst.Pipe( db.PP(cmd, unclaimedKey, eventID), db.PP("PUBLISH", channelName, eventID), ) if err != nil { return nil, fmt.Errorf("QPUSH* %s/PUBLISH: %s", cmd, err) } return okSS, nil }
func qnotify(client *clients.Client, args []string) (interface{}, error) { timeout, err := parseInt(args[0], "timeout") if err != nil { return err, nil } // ensure the NotifyCh is empty before waiting queueName := "" client.DrainNotifyCh() // check to see if we have any events in the registered queues. We check the // list in a randomized order since very active queues in the list may not // ever let us check after them in the list, abandoning the rest of the list queueNames := client.Queues for _, i := range rand.Perm(len(queueNames)) { unclaimedKey := db.UnclaimedKey(queueNames[i]) var unclaimedCount int unclaimedCount, err = db.Inst.Cmd("LLEN", unclaimedKey).Int() if err != nil { return nil, fmt.Errorf("QNOTIFY LLEN unclaimed): %s", err) } if unclaimedCount > 0 { queueName = queueNames[i] break } } if queueName == "" { select { case <-time.After(time.Duration(timeout) * time.Second): case queueName = <-client.NotifyCh: } } if queueName != "" { return queueName, nil } return nil, nil }
func TestQFlush(t *T) { client := newClient() queue := clients.RandQueueName() // Ensure adding a single event to a queue and then flushing destroys it Dispatch(client, "qlpush", []string{queue, "0", "foo"}) readAndAssertStr(t, client, "OK") Dispatch(client, "qflush", []string{queue}) readAndAssertStr(t, client, "OK") Dispatch(client, "qstatus", []string{queue}) readAndAssertQStatus(t, client, []queueInfo{ queueInfo{queue, 0, 0, 0}, }) // Ensure adding a multiple items, having one in the claimed queue, and then // flushing still destroys everything Dispatch(client, "qlpush", []string{queue, "0", "foo"}) readAndAssertStr(t, client, "OK") Dispatch(client, "qlpush", []string{queue, "1", "foo"}) readAndAssertStr(t, client, "OK") Dispatch(client, "qrpop", []string{queue}) readAndAssertArr(t, client, []string{"0", "foo"}) Dispatch(client, "qflush", []string{queue}) readAndAssertStr(t, client, "OK") Dispatch(client, "qstatus", []string{queue}) readAndAssertQStatus(t, client, []queueInfo{ queueInfo{queue, 0, 0, 0}, }) // Make sure the actual redis keys are destroyed keys := []string{ db.UnclaimedKey(queue), db.ClaimedKey(queue), db.ItemsKey(queue), } for _, key := range keys { exists, err := db.Inst.Cmd("EXISTS", key).Int() assert.Nil(t, err) assert.Equal(t, 0, exists) } }
// There's a race condition where an item get's ackd just after being found by // the restore process. The process checks if the item lock exists, it doesn't // because the ack just deleted it, and then attempts to move the item from the // claimed to the unclaimed queue. We need to make sure it doesn't appear in the // unclaimed queue in this case func TestRestoreRace(t *T) { q := clients.RandQueueName() unclaimedKey := db.UnclaimedKey(q) claimedKey := db.ClaimedKey(q) require.Nil(t, db.Inst.Cmd("LPUSH", unclaimedKey, "buz", "boz").Err) require.Nil(t, db.Inst.Cmd("LPUSH", claimedKey, "bar", "baz").Err) // restore an event which is in the claimed list, and one which is not require.Nil(t, restoreEventToQueue(q, "bar")) require.Nil(t, restoreEventToQueue(q, "foo")) l, err := db.Inst.Cmd("LRANGE", claimedKey, 0, -1).List() require.Nil(t, err) assert.Equal(t, []string{"baz"}, l) l, err = db.Inst.Cmd("LRANGE", unclaimedKey, 0, -1).List() require.Nil(t, err) assert.Equal(t, []string{"boz", "buz", "bar"}, l) }
func TestRestore(t *T) { // Add an item to claimed list and call validateClaimedEvents. Since it // doesn't have a lock it should be moved to the unclaimed list q := clients.RandQueueName() unclaimedKey := db.UnclaimedKey(q) claimedKey := db.ClaimedKey(q) itemsKey := db.ItemsKey(q) require.Nil(t, db.Inst.Cmd("HSET", itemsKey, "foo", "bar").Err) require.Nil(t, db.Inst.Cmd("LPUSH", claimedKey, "foo").Err) validateClaimedEvents() l, err := db.Inst.Cmd("LRANGE", claimedKey, 0, -1).List() require.Nil(t, err) assert.Empty(t, l) l, err = db.Inst.Cmd("LRANGE", unclaimedKey, 0, -1).List() require.Nil(t, err) assert.Equal(t, []string{"foo"}, l) }
func qack(client *clients.Client, args []string) (interface{}, error) { queueName, eventID := args[0], args[1] claimedKey := db.ClaimedKey(queueName) var numRemoved int numRemoved, err := db.Inst.Cmd("LREM", claimedKey, -1, eventID).Int() if err != nil { return nil, fmt.Errorf("QACK LREM (claimed): %s", err) } // If we didn't removed the eventID from the claimed events we see if it can // be found in unclaimed. We only do this in the uncommon case that the // eventID isn't in claimed since unclaimed can be really large, so LREM is // slow on it if numRemoved == 0 { unclaimedKey := db.UnclaimedKey(queueName) var err error numRemoved, err = db.Inst.Cmd("LREM", unclaimedKey, -1, eventID).Int() if err != nil { return nil, fmt.Errorf("QACK LREM (unclaimed): %s", err) } } // We only remove the object data itself if the eventID was actually removed // from something if numRemoved > 0 { itemsKey := db.ItemsKey(queueName) lockKey := db.ItemLockKey(queueName, eventID) _, err := db.Inst.Pipe( db.PP("HDEL", itemsKey, eventID), db.PP("DEL", lockKey), ) if err != nil { return nil, fmt.Errorf("QACK HDEL/DEL: %s", err) } } return numRemoved, nil }
func qrpop(client *clients.Client, args []string) (interface{}, error) { var err error queueName := args[0] expires := 30 noack := false args = args[1:] if len(args) > 1 && isEqualUpper("EX", args[0]) { if expires, err = parseInt(args[1], "expires"); err != nil { return err, nil } // The expiry of 0 is special and turns the read into at-most-once if expires == 0 { noack = true expires = 30 } } unclaimedKey := db.UnclaimedKey(queueName) claimedKey := db.ClaimedKey(queueName) itemsKey := db.ItemsKey(queueName) reply := db.Inst.Lua("RPOPLPUSH_LOCK_HGET", 3, unclaimedKey, claimedKey, itemsKey, queueName, expires) if reply.IsType(redis.Nil) { return nil, nil } r, err := reply.List() if err != nil { return nil, fmt.Errorf("QRPOP RPOPLPUSH_LOCK_HGET: %s", err) } if noack { qack(client, []string{queueName, r[0]}) } return r, nil }
func restoreEventToQueue(queueName string, eventID string) error { unclaimedKey := db.UnclaimedKey(queueName) claimedKey := db.ClaimedKey(queueName) r := db.Inst.Lua("LREMRPUSH", 2, claimedKey, unclaimedKey, eventID) return r.Err }