From 23339505d07422de9587b8918c2486aa393a3b66 Mon Sep 17 00:00:00 2001 From: The Things Bot Date: Mon, 12 Jan 2026 07:17:48 +0000 Subject: [PATCH 1/5] all: Bump to version 3.35.2 --- CHANGELOG.md | 4 ++-- data/lorawan-devices | 2 +- data/lorawan-webhook-templates | 2 +- package.json | 2 +- pkg/version/ttn.go | 2 +- sdk/js/package.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 599ce3e586..8876db9a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,13 +23,13 @@ For details about compatibility between different releases, see the **Commitment ## [3.35.2] - unreleased +## [3.35.1] - 2025-12-19 + ### Added - Add HSTS response headers. - Draft band definition for Uzbekistan 923Mhz band. -## [3.35.1] - 2025-12-19 - ## [3.35.0] - 2025-11-19 ### Added diff --git a/data/lorawan-devices b/data/lorawan-devices index 2857b28165..dd8c16d7e9 160000 --- a/data/lorawan-devices +++ b/data/lorawan-devices @@ -1 +1 @@ -Subproject commit 2857b28165e74bcae928e2b3b05dd61eba3dd123 +Subproject commit dd8c16d7e92fc30a3cc16026e419c54a99536087 diff --git a/data/lorawan-webhook-templates b/data/lorawan-webhook-templates index 509f33d2e4..8c2b19fa82 160000 --- a/data/lorawan-webhook-templates +++ b/data/lorawan-webhook-templates @@ -1 +1 @@ -Subproject commit 509f33d2e4dc5baa5b88a4e2b48deebcaf412b90 +Subproject commit 8c2b19fa82f209603bed14917487704b9ecef405 diff --git a/package.json b/package.json index 29d54e90db..42a8b8a0ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ttn-stack", - "version": "3.35.1", + "version": "3.35.2", "description": "The Things Stack", "main": "index.js", "repository": "https://github.com/TheThingsNetwork/lorawan-stack.git", diff --git a/pkg/version/ttn.go b/pkg/version/ttn.go index 234525f141..dac2162eb2 100644 --- a/pkg/version/ttn.go +++ b/pkg/version/ttn.go @@ -3,4 +3,4 @@ package version // TTN Version -var TTN = "3.35.1-dev" +var TTN = "3.35.2-dev" diff --git a/sdk/js/package.json b/sdk/js/package.json index 6835e49785..a35cdd2512 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "ttn-lw", - "version": "3.35.1", + "version": "3.35.2", "description": "The Things Stack for LoRaWAN JavaScript SDK", "url": "https://github.com/TheThingsNetwork/lorawan-stack/tree/default/sdk/js", "main": "dist/index.js", From fd4baa5281b531168ab2b5899f7f5146e42f7017 Mon Sep 17 00:00:00 2001 From: Aleksander Borowski Date: Thu, 15 Jan 2026 10:25:31 +0100 Subject: [PATCH 2/5] all: Update `data/lorawan-frequency-plans` --- data/lorawan-frequency-plans | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/lorawan-frequency-plans b/data/lorawan-frequency-plans index 503a9774d8..52f4a7ed44 160000 --- a/data/lorawan-frequency-plans +++ b/data/lorawan-frequency-plans @@ -1 +1 @@ -Subproject commit 503a9774d84140e7e177a6b3a025b9f25412681d +Subproject commit 52f4a7ed44e766252da59df26ddac0cbeeb13e43 From de8e6be382f5a6567c4fb28ced1796033a4a056a Mon Sep 17 00:00:00 2001 From: Nicholas Cristofaro Date: Tue, 13 Jan 2026 09:18:04 -0300 Subject: [PATCH 3/5] Merge pull request #7809 from TheThingsNetwork/fix/downlink-queue-redis-exhaustion Update DownlinkQueue operations RateLimit keys --- CHANGELOG.md | 4 ++++ pkg/ttnpb/application_interfaces.go | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8876db9a4e..08d8983f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ For details about compatibility between different releases, see the **Commitment ## [3.35.2] - unreleased +### Changed + +- Rate limiting for downlink queue operations (`DownlinkQueuePush`, `DownlinkQueueReplace`) is now applied at the application level instead of per-device. This may result in more `ResourceExhausted` (429) errors when multiple devices under the same application perform downlink queue operations concurrently. + ## [3.35.1] - 2025-12-19 ### Added diff --git a/pkg/ttnpb/application_interfaces.go b/pkg/ttnpb/application_interfaces.go index 45c34e305d..c80a4c58b7 100644 --- a/pkg/ttnpb/application_interfaces.go +++ b/pkg/ttnpb/application_interfaces.go @@ -210,3 +210,13 @@ func (m *DownlinkQueueRequest) IDString() string { } return ids.IDString() } + +// RateLimitKey implements ratelimit.Keyer. +// Returns an application-level key to enable rate limiting per application for downlink queue operations. +func (m *DownlinkQueueRequest) RateLimitKey() string { + ids := m.EndDeviceIds + if ids == nil { + return "" + } + return "application:" + ids.GetApplicationIds().IDString() +} From 55c946ee92738c6156ed09b9c04850396f3e7485 Mon Sep 17 00:00:00 2001 From: Nicholas Cristofaro Date: Tue, 13 Jan 2026 09:58:23 -0300 Subject: [PATCH 4/5] Merge pull request #7810 from TheThingsNetwork/feat/as-redis-list-watch-update Backport of Redis webhook List operations --- CHANGELOG.md | 4 +++ .../io/web/redis/registry.go | 31 ++++++++-------- .../io/web/redis/registry_test.go | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d8983f3b..705273dfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ For details about compatibility between different releases, see the **Commitment - Rate limiting for downlink queue operations (`DownlinkQueuePush`, `DownlinkQueueReplace`) is now applied at the application level instead of per-device. This may result in more `ResourceExhausted` (429) errors when multiple devices under the same application perform downlink queue operations concurrently. +### Fixed + +- Application Server webhook registry now uses read-only Redis client for non-paginated list operations, reducing connection holding time during high traffic. + ## [3.35.1] - 2025-12-19 ### Added diff --git a/pkg/applicationserver/io/web/redis/registry.go b/pkg/applicationserver/io/web/redis/registry.go index 937ed2b01a..1fbc4abb32 100644 --- a/pkg/applicationserver/io/web/redis/registry.go +++ b/pkg/applicationserver/io/web/redis/registry.go @@ -102,16 +102,19 @@ func (r WebhookRegistry) List(ctx context.Context, ids *ttnpb.ApplicationIdentif appUID := unique.ID(ctx, ids) uidKey := r.appKey(appUID) - opts := []ttnredis.FindProtosOption{} + defer trace.StartRegion(ctx, "list webhooks by application id").End() + limit, offset := ttnredis.PaginationLimitAndOffsetFromContext(ctx) - if limit != 0 { - opts = append(opts, - ttnredis.FindProtosSorted(true), - ttnredis.FindProtosWithOffsetAndCount(offset, limit), - ) - } rangeProtos := func(c redis.Cmdable) error { + var opts []ttnredis.FindProtosOption + if limit != 0 { + opts = append(opts, + ttnredis.FindProtosSorted(true), + ttnredis.FindProtosWithOffsetAndCount(offset, limit), + ) + } + pbs = make([]*ttnpb.ApplicationWebhook, 0) return ttnredis.FindProtos(ctx, c, uidKey, r.makeIDKeyFunc(appUID), opts...).Range( func() (proto.Message, func() (bool, error)) { pb := &ttnpb.ApplicationWebhook{} @@ -126,27 +129,21 @@ func (r WebhookRegistry) List(ctx context.Context, ids *ttnpb.ApplicationIdentif }) } - defer trace.StartRegion(ctx, "list webhooks by application id").End() - var err error if limit != 0 { - var lockerID string - lockerID, err = ttnredis.GenerateLockerID() - if err != nil { - return nil, err - } - err = ttnredis.LockedWatch(ctx, r.Redis, uidKey, lockerID, r.LockTTL, func(tx *redis.Tx) (err error) { + // Pagination requires Watch() for consistency between SCard and SORT. + err = r.Redis.Watch(ctx, func(tx *redis.Tx) error { total, err := tx.SCard(ctx, uidKey).Result() if err != nil { return err } ttnredis.SetPaginationTotal(ctx, total) return rangeProtos(tx) - }) + }, uidKey) } else { + // No pagination - use client without Watch() to reduce connection holding time. err = rangeProtos(r.Redis) } - if err != nil { return nil, ttnredis.ConvertError(err) } diff --git a/pkg/applicationserver/io/web/redis/registry_test.go b/pkg/applicationserver/io/web/redis/registry_test.go index 696a141e33..8de32b1397 100644 --- a/pkg/applicationserver/io/web/redis/registry_test.go +++ b/pkg/applicationserver/io/web/redis/registry_test.go @@ -179,6 +179,42 @@ func TestWebhookRegistry(t *testing.T) { a.So(webhooks[0].Format, should.Equal, format) }) + t.Run("ListMultipleWithoutPagination", func(t *testing.T) { + t.Parallel() + a, ctx := test.New(t) + + appID := &ttnpb.ApplicationIdentifiers{ + ApplicationId: "myapp-list-multiple", + } + + // Create multiple webhooks + for i := 1; i <= 5; i++ { + whIDs := &ttnpb.ApplicationWebhookIdentifiers{ + ApplicationIds: appID, + WebhookId: fmt.Sprintf("webhook-%02d", i), + } + _, err := registry.Set(ctx, whIDs, paths, + func(*ttnpb.ApplicationWebhook) (*ttnpb.ApplicationWebhook, []string, error) { + return &ttnpb.ApplicationWebhook{ + Ids: whIDs, + Format: format, + BaseUrl: baseURL, + }, paths, nil + }, + ) + a.So(err, should.BeNil) + } + + // List without pagination context - exercises ReadOnlyClient() path + webhooks, err := registry.List(ctx, appID, paths) + a.So(err, should.BeNil) + a.So(webhooks, should.HaveLength, 5) + for _, wh := range webhooks { + a.So(wh.Ids.ApplicationIds, should.Resemble, appID) + a.So(wh.Format, should.Equal, format) + } + }) + t.Run("Pagination", func(t *testing.T) { t.Parallel() a, ctx := test.New(t) From 1d3afedebd5ddf02e6984be7d7f3a830f760a1ed Mon Sep 17 00:00:00 2001 From: The Things Bot Date: Fri, 30 Jan 2026 13:32:24 +0000 Subject: [PATCH 5/5] all: Enter release date of version 3.35.2 into the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 705273dfa9..2c49c16e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ For details about compatibility between different releases, see the **Commitment ### Security -## [3.35.2] - unreleased +## [3.35.2] - 2026-01-30 ### Changed