|
6 | 6 | "crypto/sha1" |
7 | 7 | "encoding/json" |
8 | 8 | "fmt" |
| 9 | + "math" |
9 | 10 | "reflect" |
10 | 11 | "strconv" |
11 | 12 | "time" |
@@ -7625,6 +7626,206 @@ var _ = Describe("Commands", func() { |
7625 | 7626 | Expect(n).To(Equal(int64(2))) |
7626 | 7627 | }) |
7627 | 7628 |
|
| 7629 | + It("should not XNack with no mode", func() { |
| 7630 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7631 | + |
| 7632 | + // Mode is required by Redis; omitting it should return an error. |
| 7633 | + _, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7634 | + Stream: "stream", |
| 7635 | + Group: "group", |
| 7636 | + IDs: []string{"1-0", "2-0"}, |
| 7637 | + }).Result() |
| 7638 | + Expect(err).To(HaveOccurred()) |
| 7639 | + Expect(err.Error()).To(ContainSubstring("mode must be SILENT, FAIL, or FATAL")) |
| 7640 | + }) |
| 7641 | + |
| 7642 | + It("should XNack with SILENT mode", func() { |
| 7643 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7644 | + |
| 7645 | + // All 3 messages are pending (delivered by BeforeEach), each with delivery_count=1. |
| 7646 | + // SILENT: consumer shutting down; delivery counter decremented by 1 (1 → 0). |
| 7647 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7648 | + Stream: "stream", |
| 7649 | + Group: "group", |
| 7650 | + Mode: "SILENT", |
| 7651 | + IDs: []string{"1-0", "2-0"}, |
| 7652 | + }).Result() |
| 7653 | + Expect(err).NotTo(HaveOccurred()) |
| 7654 | + Expect(n).To(Equal(int64(2))) |
| 7655 | + |
| 7656 | + // NACKed messages move to unassigned "nacked" state in the PEL. |
| 7657 | + // Total PEL count stays 3; only 3-0 remains assigned to consumer. |
| 7658 | + pendingInfo, err := client.XPending(ctx, "stream", "group").Result() |
| 7659 | + Expect(err).NotTo(HaveOccurred()) |
| 7660 | + Expect(pendingInfo.Count).To(Equal(int64(3))) |
| 7661 | + Expect(pendingInfo.Consumers).To(Equal(map[string]int64{"consumer": 1})) |
| 7662 | + |
| 7663 | + // Verify the delivery counter was decremented from 1 to 0 for the NACKed messages. |
| 7664 | + infoExt, err := client.XPendingExt(ctx, &redis.XPendingExtArgs{ |
| 7665 | + Stream: "stream", |
| 7666 | + Group: "group", |
| 7667 | + Start: "-", |
| 7668 | + End: "+", |
| 7669 | + Count: 10, |
| 7670 | + }).Result() |
| 7671 | + Expect(err).NotTo(HaveOccurred()) |
| 7672 | + for _, e := range infoExt { |
| 7673 | + if e.ID == "1-0" || e.ID == "2-0" { |
| 7674 | + Expect(e.RetryCount).To(Equal(int64(0)), "SILENT should decrement delivery counter to 0 for %s", e.ID) |
| 7675 | + } |
| 7676 | + } |
| 7677 | + }) |
| 7678 | + |
| 7679 | + It("should XNack with FAIL mode", func() { |
| 7680 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7681 | + |
| 7682 | + // FAIL: delivery counter stays the same (1 → 1). |
| 7683 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7684 | + Stream: "stream", |
| 7685 | + Group: "group", |
| 7686 | + Mode: "FAIL", |
| 7687 | + IDs: []string{"1-0"}, |
| 7688 | + }).Result() |
| 7689 | + Expect(err).NotTo(HaveOccurred()) |
| 7690 | + Expect(n).To(Equal(int64(1))) |
| 7691 | + |
| 7692 | + // NACKed message moves to unassigned "nacked" state. |
| 7693 | + // Only 2 messages (2-0, 3-0) remain assigned to consumer. |
| 7694 | + pendingInfo, err := client.XPending(ctx, "stream", "group").Result() |
| 7695 | + Expect(err).NotTo(HaveOccurred()) |
| 7696 | + Expect(pendingInfo.Count).To(Equal(int64(3))) |
| 7697 | + Expect(pendingInfo.Consumers).To(Equal(map[string]int64{"consumer": 2})) |
| 7698 | + |
| 7699 | + // Verify the delivery counter was left unchanged at 1. |
| 7700 | + infoExt, err := client.XPendingExt(ctx, &redis.XPendingExtArgs{ |
| 7701 | + Stream: "stream", |
| 7702 | + Group: "group", |
| 7703 | + Start: "-", |
| 7704 | + End: "+", |
| 7705 | + Count: 10, |
| 7706 | + }).Result() |
| 7707 | + Expect(err).NotTo(HaveOccurred()) |
| 7708 | + for _, e := range infoExt { |
| 7709 | + if e.ID == "1-0" { |
| 7710 | + Expect(e.RetryCount).To(Equal(int64(1)), "FAIL should leave delivery counter unchanged") |
| 7711 | + } |
| 7712 | + } |
| 7713 | + }) |
| 7714 | + |
| 7715 | + It("should XNack with FATAL mode", func() { |
| 7716 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7717 | + |
| 7718 | + // FATAL: delivery counter set to MAXINT (for invalid/malicious messages). |
| 7719 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7720 | + Stream: "stream", |
| 7721 | + Group: "group", |
| 7722 | + Mode: "FATAL", |
| 7723 | + IDs: []string{"1-0"}, |
| 7724 | + }).Result() |
| 7725 | + Expect(err).NotTo(HaveOccurred()) |
| 7726 | + Expect(n).To(Equal(int64(1))) |
| 7727 | + |
| 7728 | + // NACKed message moves to unassigned state; 2-0 and 3-0 remain assigned. |
| 7729 | + pendingInfo, err := client.XPending(ctx, "stream", "group").Result() |
| 7730 | + Expect(err).NotTo(HaveOccurred()) |
| 7731 | + Expect(pendingInfo.Count).To(Equal(int64(3))) |
| 7732 | + Expect(pendingInfo.Consumers).To(Equal(map[string]int64{"consumer": 2})) |
| 7733 | + |
| 7734 | + // Verify the delivery counter was set to MAXINT (math.MaxInt64). |
| 7735 | + infoExt, err := client.XPendingExt(ctx, &redis.XPendingExtArgs{ |
| 7736 | + Stream: "stream", |
| 7737 | + Group: "group", |
| 7738 | + Start: "-", |
| 7739 | + End: "+", |
| 7740 | + Count: 10, |
| 7741 | + }).Result() |
| 7742 | + Expect(err).NotTo(HaveOccurred()) |
| 7743 | + for _, e := range infoExt { |
| 7744 | + if e.ID == "1-0" { |
| 7745 | + Expect(e.RetryCount).To(Equal(int64(math.MaxInt64)), "FATAL should set delivery counter to MAXINT") |
| 7746 | + } |
| 7747 | + } |
| 7748 | + }) |
| 7749 | + |
| 7750 | + It("should XNack nacked-count reflected in XINFO STREAM FULL", func() { |
| 7751 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7752 | + |
| 7753 | + // NACK two messages. |
| 7754 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7755 | + Stream: "stream", |
| 7756 | + Group: "group", |
| 7757 | + Mode: "FAIL", |
| 7758 | + IDs: []string{"1-0", "2-0"}, |
| 7759 | + }).Result() |
| 7760 | + Expect(err).NotTo(HaveOccurred()) |
| 7761 | + Expect(n).To(Equal(int64(2))) |
| 7762 | + |
| 7763 | + // Verify nacked-count in XINFO STREAM FULL. |
| 7764 | + info, err := client.XInfoStreamFull(ctx, "stream", 10).Result() |
| 7765 | + Expect(err).NotTo(HaveOccurred()) |
| 7766 | + Expect(info.Groups).To(HaveLen(1)) |
| 7767 | + Expect(info.Groups[0].NackedCount).To(Equal(uint64(2))) |
| 7768 | + }) |
| 7769 | + |
| 7770 | + It("should XNack with RetryCount", func() { |
| 7771 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7772 | + |
| 7773 | + retryCount := uint64(5) |
| 7774 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7775 | + Stream: "stream", |
| 7776 | + Group: "group", |
| 7777 | + Mode: "FAIL", |
| 7778 | + IDs: []string{"1-0"}, |
| 7779 | + RetryCount: &retryCount, |
| 7780 | + }).Result() |
| 7781 | + Expect(err).NotTo(HaveOccurred()) |
| 7782 | + Expect(n).To(Equal(int64(1))) |
| 7783 | + |
| 7784 | + // Verify the delivery counter was set to the explicit RetryCount value, |
| 7785 | + // overriding the mode's default adjustment. |
| 7786 | + infoExt, err := client.XPendingExt(ctx, &redis.XPendingExtArgs{ |
| 7787 | + Stream: "stream", |
| 7788 | + Group: "group", |
| 7789 | + Start: "-", |
| 7790 | + End: "+", |
| 7791 | + Count: 10, |
| 7792 | + }).Result() |
| 7793 | + Expect(err).NotTo(HaveOccurred()) |
| 7794 | + // Find 1-0 and verify its delivery counter. |
| 7795 | + var entry redis.XPendingExt |
| 7796 | + for _, e := range infoExt { |
| 7797 | + if e.ID == "1-0" { |
| 7798 | + entry = e |
| 7799 | + break |
| 7800 | + } |
| 7801 | + } |
| 7802 | + Expect(entry.ID).To(Equal("1-0")) |
| 7803 | + Expect(entry.RetryCount).To(Equal(int64(retryCount))) |
| 7804 | + }) |
| 7805 | + |
| 7806 | + It("should XNack with Force", func() { |
| 7807 | + SkipBeforeRedisVersion(8.8, "XNACK requires Redis 8.8+") |
| 7808 | + |
| 7809 | + // Force creates a new NACKed PEL entry for an ID that was never |
| 7810 | + // delivered to a consumer via XREADGROUP. Without Force this would |
| 7811 | + // be a no-op (the ID is not in any consumer's PEL). |
| 7812 | + n, err := client.XNack(ctx, &redis.XNackArgs{ |
| 7813 | + Stream: "stream", |
| 7814 | + Group: "group", |
| 7815 | + Mode: "FAIL", |
| 7816 | + IDs: []string{"1-0"}, |
| 7817 | + Force: true, |
| 7818 | + }).Result() |
| 7819 | + Expect(err).NotTo(HaveOccurred()) |
| 7820 | + Expect(n).To(Equal(int64(1))) |
| 7821 | + |
| 7822 | + // The entry is now in the group's PEL as an unassigned NACKed entry. |
| 7823 | + pendingInfo, err := client.XPending(ctx, "stream", "group").Result() |
| 7824 | + Expect(err).NotTo(HaveOccurred()) |
| 7825 | + Expect(pendingInfo.Count).To(Equal(int64(3))) |
| 7826 | + Expect(pendingInfo.Consumers).To(Equal(map[string]int64{"consumer": 2})) |
| 7827 | + }) |
| 7828 | + |
7628 | 7829 | It("should XReadGroup with CLAIM argument", func() { |
7629 | 7830 | SkipBeforeRedisVersion(8.3, "XREADGROUP CLAIM requires Redis 8.3+") |
7630 | 7831 |
|
|
0 commit comments