• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 16665047018

31 Jul 2025 06:34PM UTC coverage: 86.897% (+0.1%) from 86.781%
16665047018

push

github

web-flow
Apigw/enable vpce routing (#12937)

5 of 5 new or added lines in 1 file covered. (100.0%)

314 existing lines in 13 files now uncovered.

66469 of 76492 relevant lines covered (86.9%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.8
/localstack-core/localstack/services/sqs/models.py
1
import hashlib
1✔
2
import heapq
1✔
3
import inspect
1✔
4
import json
1✔
5
import logging
1✔
6
import re
1✔
7
import threading
1✔
8
import time
1✔
9
from datetime import datetime
1✔
10
from queue import Empty
1✔
11
from typing import Dict, Optional, Set
1✔
12

13
from localstack import config
1✔
14
from localstack.aws.api import RequestContext
1✔
15
from localstack.aws.api.sqs import (
1✔
16
    AttributeNameList,
17
    InvalidAttributeName,
18
    Message,
19
    MessageSystemAttributeName,
20
    QueueAttributeMap,
21
    QueueAttributeName,
22
    ReceiptHandleIsInvalid,
23
    TagMap,
24
)
25
from localstack.services.sqs import constants as sqs_constants
1✔
26
from localstack.services.sqs.exceptions import (
1✔
27
    InvalidAttributeValue,
28
    InvalidParameterValueException,
29
    MissingRequiredParameterException,
30
)
31
from localstack.services.sqs.queue import InterruptiblePriorityQueue, InterruptibleQueue
1✔
32
from localstack.services.sqs.utils import (
1✔
33
    encode_move_task_handle,
34
    encode_receipt_handle,
35
    extract_receipt_handle_info,
36
    global_message_sequence,
37
    guess_endpoint_strategy_and_host,
38
    is_message_deduplication_id_required,
39
)
40
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
1✔
41
from localstack.utils.aws.arns import get_partition
1✔
42
from localstack.utils.strings import long_uid
1✔
43
from localstack.utils.time import now
1✔
44
from localstack.utils.urls import localstack_host
1✔
45

46
LOG = logging.getLogger(__name__)
1✔
47

48
ReceiptHandle = str
1✔
49

50

51
class SqsMessage:
1✔
52
    message: Message
1✔
53
    created: float
1✔
54
    visibility_timeout: int
1✔
55
    receive_count: int
1✔
56
    delay_seconds: Optional[int]
1✔
57
    receipt_handles: Set[str]
1✔
58
    last_received: Optional[float]
1✔
59
    first_received: Optional[float]
1✔
60
    visibility_deadline: Optional[float]
1✔
61
    deleted: bool
1✔
62
    priority: float
1✔
63
    message_deduplication_id: str
1✔
64
    message_group_id: str
1✔
65
    sequence_number: str
1✔
66

67
    def __init__(
1✔
68
        self,
69
        priority: float,
70
        message: Message,
71
        message_deduplication_id: str = None,
72
        message_group_id: str = None,
73
        sequence_number: str = None,
74
    ) -> None:
75
        self.created = time.time()
1✔
76
        self.message = message
1✔
77
        self.receive_count = 0
1✔
78
        self.receipt_handles = set()
1✔
79

80
        self.delay_seconds = None
1✔
81
        self.last_received = None
1✔
82
        self.first_received = None
1✔
83
        self.visibility_deadline = None
1✔
84
        self.deleted = False
1✔
85
        self.priority = priority
1✔
86
        self.sequence_number = sequence_number
1✔
87

88
        attributes = {}
1✔
89
        if message_group_id is not None:
1✔
90
            attributes["MessageGroupId"] = message_group_id
1✔
91
        if message_deduplication_id is not None:
1✔
92
            attributes["MessageDeduplicationId"] = message_deduplication_id
1✔
93
        if sequence_number is not None:
1✔
94
            attributes["SequenceNumber"] = sequence_number
1✔
95

96
        if self.message.get("Attributes"):
1✔
97
            self.message["Attributes"].update(attributes)
1✔
98
        else:
99
            self.message["Attributes"] = attributes
×
100

101
        # set attribute default values if not set
102
        self.message["Attributes"].setdefault(
1✔
103
            MessageSystemAttributeName.ApproximateReceiveCount, "0"
104
        )
105

106
    @property
1✔
107
    def message_group_id(self) -> Optional[str]:
1✔
108
        return self.message["Attributes"].get(MessageSystemAttributeName.MessageGroupId)
1✔
109

110
    @property
1✔
111
    def message_deduplication_id(self) -> Optional[str]:
1✔
112
        return self.message["Attributes"].get(MessageSystemAttributeName.MessageDeduplicationId)
1✔
113

114
    @property
1✔
115
    def dead_letter_queue_source_arn(self) -> Optional[str]:
1✔
116
        return self.message["Attributes"].get(MessageSystemAttributeName.DeadLetterQueueSourceArn)
1✔
117

118
    @property
1✔
119
    def message_id(self):
1✔
120
        return self.message["MessageId"]
1✔
121

122
    def increment_approximate_receive_count(self):
1✔
123
        """
124
        Increment the message system attribute ``ApproximateReceiveCount``.
125
        """
126
        # TODO: need better handling of system attributes
127
        cnt = int(
1✔
128
            self.message["Attributes"].get(MessageSystemAttributeName.ApproximateReceiveCount, "0")
129
        )
130
        cnt += 1
1✔
131
        self.message["Attributes"][MessageSystemAttributeName.ApproximateReceiveCount] = str(cnt)
1✔
132

133
    def set_last_received(self, timestamp: float):
1✔
134
        """
135
        Sets the last received timestamp of the message to the given value, and updates the visibility deadline
136
        accordingly.
137

138
        :param timestamp: the last time the message was received
139
        """
140
        self.last_received = timestamp
1✔
141
        self.visibility_deadline = timestamp + self.visibility_timeout
1✔
142

143
    def update_visibility_timeout(self, timeout: int):
1✔
144
        """
145
        Sets the visibility timeout of the message to the given value, and updates the visibility deadline accordingly.
146

147
        :param timeout: the timeout value in seconds
148
        """
149
        self.visibility_timeout = timeout
1✔
150
        self.visibility_deadline = time.time() + timeout
1✔
151

152
    @property
1✔
153
    def is_visible(self) -> bool:
1✔
154
        """
155
        Returns false if the message has a visibility deadline that is in the future.
156

157
        :return: whether the message is visibile or not.
158
        """
159
        if self.visibility_deadline is None:
1✔
160
            return True
1✔
161
        if time.time() >= self.visibility_deadline:
1✔
162
            return True
1✔
163

164
        return False
1✔
165

166
    @property
1✔
167
    def is_delayed(self) -> bool:
1✔
168
        if self.delay_seconds is None:
1✔
169
            return False
×
170
        return time.time() <= self.created + self.delay_seconds
1✔
171

172
    def __gt__(self, other):
1✔
173
        return self.priority > other.priority
×
174

175
    def __ge__(self, other):
1✔
176
        return self.priority >= other.priority
×
177

178
    def __lt__(self, other):
1✔
179
        return self.priority < other.priority
1✔
180

181
    def __le__(self, other):
1✔
182
        return self.priority <= other.priority
×
183

184
    def __eq__(self, other):
1✔
185
        return self.message_id == other.message_id
×
186

187
    def __hash__(self):
1✔
188
        return self.message_id.__hash__()
1✔
189

190
    def __repr__(self):
191
        return f"SqsMessage(id={self.message_id},group={self.message_group_id})"
192

193

194
class ReceiveMessageResult:
1✔
195
    """
196
    Object to communicate the result of a "receive messages" operation between the SqsProvider and
197
    the underlying datastructure holding the messages.
198
    """
199

200
    successful: list[SqsMessage]
1✔
201
    """The messages that were successfully received from the queue"""
1✔
202

203
    receipt_handles: list[str]
1✔
204
    """The array index position in ``successful`` and ``receipt_handles`` need to be the same (this
1✔
205
    assumption is needed when assembling the result in `SqsProvider.receive_message`)"""
206

207
    dead_letter_messages: list[SqsMessage]
1✔
208
    """All messages that were received more than maxReceiveCount in the redrive policy (if any)"""
1✔
209

210
    def __init__(self):
1✔
211
        self.successful = []
1✔
212
        self.receipt_handles = []
1✔
213
        self.dead_letter_messages = []
1✔
214

215

216
class MessageMoveTaskStatus(str):
1✔
217
    CREATED = "CREATED"  # not validated, for internal use
1✔
218
    RUNNING = "RUNNING"
1✔
219
    COMPLETED = "COMPLETED"
1✔
220
    CANCELLING = "CANCELLING"
1✔
221
    CANCELLED = "CANCELLED"
1✔
222
    FAILED = "FAILED"
1✔
223

224

225
class MessageMoveTask:
1✔
226
    """
227
    A task created by the ``StartMessageMoveTask`` operation.
228
    """
229

230
    # configurable fields
231
    source_arn: str
1✔
232
    """The arn of the DLQ the messages are currently in."""
1✔
233
    destination_arn: str | None = None
1✔
234
    """If the DestinationArn is not specified, the original source arn will be used as target."""
1✔
235
    max_number_of_messages_per_second: int | None = None
1✔
236

237
    # dynamic fields
238
    task_id: str
1✔
239
    status: str = MessageMoveTaskStatus.CREATED
1✔
240
    started_timestamp: datetime | None = None
1✔
241
    approximate_number_of_messages_moved: int | None = None
1✔
242
    approximate_number_of_messages_to_move: int | None = None
1✔
243
    failure_reason: str | None = None
1✔
244

245
    cancel_event: threading.Event
1✔
246

247
    def __init__(
1✔
248
        self, source_arn: str, destination_arn: str, max_number_of_messages_per_second: int = None
249
    ):
250
        self.task_id = long_uid()
1✔
251
        self.source_arn = source_arn
1✔
252
        self.destination_arn = destination_arn
1✔
253
        self.max_number_of_messages_per_second = max_number_of_messages_per_second
1✔
254
        self.cancel_event = threading.Event()
1✔
255

256
    def mark_started(self):
1✔
257
        self.started_timestamp = datetime.utcnow()
1✔
258
        self.status = MessageMoveTaskStatus.RUNNING
1✔
259
        self.cancel_event.clear()
1✔
260

261
    @property
1✔
262
    def task_handle(self) -> str:
1✔
263
        return encode_move_task_handle(self.task_id, self.source_arn)
1✔
264

265

266
class SqsQueue:
1✔
267
    name: str
1✔
268
    region: str
1✔
269
    account_id: str
1✔
270

271
    attributes: QueueAttributeMap
1✔
272
    tags: TagMap
1✔
273

274
    purge_in_progress: bool
1✔
275
    purge_timestamp: Optional[float]
1✔
276

277
    delayed: Set[SqsMessage]
1✔
278
    inflight: Set[SqsMessage]
1✔
279
    receipts: Dict[str, SqsMessage]
1✔
280

281
    def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
1✔
282
        self.name = name
1✔
283
        self.region = region
1✔
284
        self.account_id = account_id
1✔
285

286
        self._assert_queue_name(name)
1✔
287
        self.tags = tags or {}
1✔
288

289
        self.delayed = set()
1✔
290
        self.inflight = set()
1✔
291
        self.receipts = {}
1✔
292

293
        self.attributes = self.default_attributes()
1✔
294
        if attributes:
1✔
295
            self.validate_queue_attributes(attributes)
1✔
296
            self.attributes.update(attributes)
1✔
297

298
        self.purge_in_progress = False
1✔
299
        self.purge_timestamp = None
1✔
300

301
        self.permissions = set()
1✔
302
        self.mutex = threading.RLock()
1✔
303

304
    def shutdown(self):
1✔
305
        pass
×
306

307
    def default_attributes(self) -> QueueAttributeMap:
1✔
308
        return {
1✔
309
            QueueAttributeName.ApproximateNumberOfMessages: lambda: str(
310
                self.approx_number_of_messages
311
            ),
312
            QueueAttributeName.ApproximateNumberOfMessagesNotVisible: lambda: str(
313
                self.approx_number_of_messages_not_visible
314
            ),
315
            QueueAttributeName.ApproximateNumberOfMessagesDelayed: lambda: str(
316
                self.approx_number_of_messages_delayed
317
            ),
318
            QueueAttributeName.CreatedTimestamp: str(now()),
319
            QueueAttributeName.DelaySeconds: "0",
320
            QueueAttributeName.LastModifiedTimestamp: str(now()),
321
            QueueAttributeName.MaximumMessageSize: str(sqs_constants.DEFAULT_MAXIMUM_MESSAGE_SIZE),
322
            QueueAttributeName.MessageRetentionPeriod: "345600",
323
            QueueAttributeName.QueueArn: self.arn,
324
            QueueAttributeName.ReceiveMessageWaitTimeSeconds: "0",
325
            QueueAttributeName.VisibilityTimeout: "30",
326
            QueueAttributeName.SqsManagedSseEnabled: "true",
327
        }
328

329
    def update_delay_seconds(self, value: int):
1✔
330
        """
331
        For standard queues, the per-queue delay setting is not retroactive—changing the setting doesn't affect the
332
        delay of messages already in the queue. For FIFO queues, the per-queue delay setting is retroactive—changing
333
        the setting affects the delay of messages already in the queue.
334

335
        https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-delay-queues.html
336

337
        :param value: the number of seconds
338
        """
339
        self.attributes[QueueAttributeName.DelaySeconds] = str(value)
×
340

341
    def update_last_modified(self, timestamp: int = None):
1✔
342
        if timestamp is None:
×
343
            timestamp = now()
×
344

345
        self.attributes[QueueAttributeName.LastModifiedTimestamp] = str(timestamp)
×
346

347
    @property
1✔
348
    def arn(self) -> str:
1✔
349
        return f"arn:{get_partition(self.region)}:sqs:{self.region}:{self.account_id}:{self.name}"
1✔
350

351
    def url(self, context: RequestContext) -> str:
1✔
352
        """Return queue URL which depending on the endpoint strategy returns e.g.:
353
        * (standard) http://sqs.eu-west-1.localhost.localstack.cloud:4566/000000000000/myqueue
354
        * (domain) http://eu-west-1.queue.localhost.localstack.cloud:4566/000000000000/myqueue
355
        * (path) http://localhost.localstack.cloud:4566/queue/eu-central-1/000000000000/myqueue
356
        * otherwise: http://localhost.localstack.cloud:4566/000000000000/myqueue
357
        """
358

359
        scheme = config.get_protocol()  # TODO: should probably change to context.request.scheme
1✔
360
        host_definition = localstack_host()
1✔
361
        host_and_port = host_definition.host_and_port()
1✔
362

363
        endpoint_strategy = config.SQS_ENDPOINT_STRATEGY
1✔
364

365
        if endpoint_strategy == "dynamic":
1✔
366
            scheme = context.request.scheme
×
367
            # determine the endpoint strategy that should be used, and determine the host dynamically
368
            endpoint_strategy, host_and_port = guess_endpoint_strategy_and_host(
×
369
                context.request.host
370
            )
371

372
        if endpoint_strategy == "standard":
1✔
373
            # Region is always part of the queue URL
374
            # sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/my-queue
375
            scheme = context.request.scheme
1✔
376
            host_url = f"{scheme}://sqs.{self.region}.{host_and_port}"
1✔
377
        elif endpoint_strategy == "domain":
1✔
378
            # Legacy style
379
            # queue.localhost.localstack.cloud:4566/000000000000/my-queue (us-east-1)
380
            # or us-east-2.queue.localhost.localstack.cloud:4566/000000000000/my-queue
381
            region = "" if self.region == "us-east-1" else self.region + "."
1✔
382
            host_url = f"{scheme}://{region}queue.{host_and_port}"
1✔
383
        elif endpoint_strategy == "path":
1✔
384
            # https?://localhost:4566/queue/us-east-1/00000000000/my-queue (us-east-1)
385
            host_url = f"{scheme}://{host_and_port}/queue/{self.region}"
1✔
386
        else:
387
            host_url = f"{scheme}://{host_and_port}"
1✔
388

389
        return "{host}/{account_id}/{name}".format(
1✔
390
            host=host_url.rstrip("/"),
391
            account_id=self.account_id,
392
            name=self.name,
393
        )
394

395
    @property
1✔
396
    def redrive_policy(self) -> Optional[dict]:
1✔
397
        if policy_document := self.attributes.get(QueueAttributeName.RedrivePolicy):
1✔
398
            return json.loads(policy_document)
1✔
399
        return None
1✔
400

401
    @property
1✔
402
    def max_receive_count(self) -> Optional[int]:
1✔
403
        """
404
        Returns the maxReceiveCount attribute of the redrive policy. If no redrive policy is set, then it
405
        returns None.
406
        """
407
        if redrive_policy := self.redrive_policy:
1✔
408
            return int(redrive_policy["maxReceiveCount"])
1✔
409
        return None
1✔
410

411
    @property
1✔
412
    def visibility_timeout(self) -> int:
1✔
413
        return int(self.attributes[QueueAttributeName.VisibilityTimeout])
1✔
414

415
    @property
1✔
416
    def delay_seconds(self) -> int:
1✔
417
        return int(self.attributes[QueueAttributeName.DelaySeconds])
1✔
418

419
    @property
1✔
420
    def wait_time_seconds(self) -> int:
1✔
421
        return int(self.attributes[QueueAttributeName.ReceiveMessageWaitTimeSeconds])
1✔
422

423
    @property
1✔
424
    def message_retention_period(self) -> int:
1✔
425
        """
426
        ``MessageRetentionPeriod`` -- the length of time, in seconds, for which Amazon SQS retains a message. Valid
427
        values: An integer representing seconds, from 60 (1 minute) to 1,209,600 (14 days). Default: 345,600 (4 days).
428
        """
429
        return int(self.attributes[QueueAttributeName.MessageRetentionPeriod])
1✔
430

431
    @property
1✔
432
    def maximum_message_size(self) -> int:
1✔
433
        return int(self.attributes[QueueAttributeName.MaximumMessageSize])
1✔
434

435
    @property
1✔
436
    def approx_number_of_messages(self) -> int:
1✔
437
        raise NotImplementedError
438

439
    @property
1✔
440
    def approx_number_of_messages_not_visible(self) -> int:
1✔
441
        return len(self.inflight)
1✔
442

443
    @property
1✔
444
    def approx_number_of_messages_delayed(self) -> int:
1✔
445
        return len(self.delayed)
1✔
446

447
    def validate_receipt_handle(self, receipt_handle: str):
1✔
448
        if self.arn != extract_receipt_handle_info(receipt_handle).queue_arn:
1✔
449
            raise ReceiptHandleIsInvalid(
×
450
                f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.'
451
            )
452

453
    def update_visibility_timeout(self, receipt_handle: str, visibility_timeout: int):
1✔
454
        with self.mutex:
1✔
455
            self.validate_receipt_handle(receipt_handle)
1✔
456

457
            if receipt_handle not in self.receipts:
1✔
458
                raise InvalidParameterValueException(
1✔
459
                    f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: Message does not exist "
460
                    f"or is not available for visibility timeout change."
461
                )
462

463
            standard_message = self.receipts[receipt_handle]
1✔
464

465
            if standard_message not in self.inflight:
1✔
466
                return
1✔
467

468
            standard_message.update_visibility_timeout(visibility_timeout)
1✔
469

470
            if visibility_timeout == 0:
1✔
471
                LOG.info(
1✔
472
                    "terminating the visibility timeout of %s",
473
                    standard_message.message["MessageId"],
474
                )
475
                # Terminating the visibility timeout for a message
476
                # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html#terminating-message-visibility-timeout
477
                self.inflight.remove(standard_message)
1✔
478
                self._put_message(standard_message)
1✔
479

480
    def remove(self, receipt_handle: str):
1✔
481
        with self.mutex:
1✔
482
            self.validate_receipt_handle(receipt_handle)
1✔
483

484
            if receipt_handle not in self.receipts:
1✔
485
                LOG.debug(
1✔
486
                    "no in-flight message found for receipt handle %s in queue %s",
487
                    receipt_handle,
488
                    self.arn,
489
                )
490
                return
1✔
491

492
            standard_message = self.receipts[receipt_handle]
1✔
493
            self._pre_delete_checks(standard_message, receipt_handle)
1✔
494
            standard_message.deleted = True
1✔
495
            LOG.debug(
1✔
496
                "deleting message %s from queue %s",
497
                standard_message.message["MessageId"],
498
                self.arn,
499
            )
500

501
            # remove all handles associated with this message
502
            for handle in standard_message.receipt_handles:
1✔
503
                del self.receipts[handle]
1✔
504
            standard_message.receipt_handles.clear()
1✔
505

506
            self._on_remove_message(standard_message)
1✔
507

508
    def _on_remove_message(self, message: SqsMessage):
1✔
509
        """Hook for queue-specific logic executed when a message is removed."""
510
        pass
×
511

512
    def put(
1✔
513
        self,
514
        message: Message,
515
        visibility_timeout: int = None,
516
        message_deduplication_id: str = None,
517
        message_group_id: str = None,
518
        delay_seconds: int = None,
519
    ) -> SqsMessage:
520
        raise NotImplementedError
521

522
    def receive(
1✔
523
        self,
524
        num_messages: int = 1,
525
        wait_time_seconds: int = None,
526
        visibility_timeout: int = None,
527
        *,
528
        poll_empty_queue: bool = False,
529
    ) -> ReceiveMessageResult:
530
        """
531
        Receive ``num_messages`` from the queue, and wait at max ``wait_time_seconds``. If a visibility
532
        timeout is given, also change the visibility timeout of all received messages accordingly.
533

534
        :param num_messages: the number of messages you want to get from the underlying queue
535
        :param wait_time_seconds: the number of seconds you want to wait
536
        :param visibility_timeout: an optional new visibility timeout
537
        :param poll_empty_queue: whether to keep polling an empty queue until the duration ``wait_time_seconds`` has elapsed
538
        :return: a ReceiveMessageResult object that contains the result of the operation
539
        """
540
        raise NotImplementedError
541

542
    def clear(self):
1✔
543
        """
544
        Calls clear on all internal datastructures that hold messages and data related to them.
545
        """
546
        with self.mutex:
1✔
547
            self.inflight.clear()
1✔
548
            self.delayed.clear()
1✔
549
            self.receipts.clear()
1✔
550

551
    def _put_message(self, message: SqsMessage):
1✔
552
        """Low-level put operation to put messages into a queue and modify visibilities accordingly."""
553
        raise NotImplementedError
554

555
    def create_receipt_handle(self, message: SqsMessage) -> str:
1✔
556
        return encode_receipt_handle(self.arn, message)
1✔
557

558
    def requeue_inflight_messages(self):
1✔
559
        if not self.inflight:
1✔
560
            return
1✔
561

562
        with self.mutex:
1✔
563
            messages = [message for message in self.inflight if message.is_visible]
1✔
564
            for standard_message in messages:
1✔
565
                LOG.debug(
1✔
566
                    "re-queueing inflight messages %s into queue %s",
567
                    standard_message,
568
                    self.arn,
569
                )
570
                self.inflight.remove(standard_message)
1✔
571
                self._put_message(standard_message)
1✔
572

573
    def enqueue_delayed_messages(self):
1✔
574
        if not self.delayed:
1✔
575
            return
1✔
576

577
        with self.mutex:
1✔
578
            messages = [message for message in self.delayed if not message.is_delayed]
1✔
579
            for standard_message in messages:
1✔
580
                LOG.debug(
1✔
581
                    "enqueueing delayed messages %s into queue %s",
582
                    standard_message.message["MessageId"],
583
                    self.arn,
584
                )
585
                self.delayed.remove(standard_message)
1✔
586
                self._put_message(standard_message)
1✔
587

588
    def remove_expired_messages(self):
1✔
589
        """
590
        Removes messages from the queue whose retention period has expired.
591
        """
592
        raise NotImplementedError
593

594
    def _assert_queue_name(self, name):
1✔
595
        if not re.match(r"^[a-zA-Z0-9_-]{1,80}$", name):
1✔
596
            raise InvalidParameterValueException(
1✔
597
                "Can only include alphanumeric characters, hyphens, or underscores. 1 to 80 in length"
598
            )
599

600
    def validate_queue_attributes(self, attributes):
1✔
601
        pass
1✔
602

603
    def add_permission(self, label: str, actions: list[str], account_ids: list[str]) -> None:
1✔
604
        """
605
        Create / append to a policy for usage with the add_permission api call
606

607
        :param actions: List of actions to be included in the policy, without the SQS: prefix
608
        :param account_ids: List of account ids to be included in the policy
609
        :param label: Permission label
610
        """
611
        statement = {
1✔
612
            "Sid": label,
613
            "Effect": "Allow",
614
            "Principal": {
615
                "AWS": [
616
                    f"arn:{get_partition(self.region)}:iam::{account_id}:root"
617
                    for account_id in account_ids
618
                ]
619
                if len(account_ids) > 1
620
                else f"arn:{get_partition(self.region)}:iam::{account_ids[0]}:root"
621
            },
622
            "Action": [f"SQS:{action}" for action in actions]
623
            if len(actions) > 1
624
            else f"SQS:{actions[0]}",
625
            "Resource": self.arn,
626
        }
627
        if policy := self.attributes.get(QueueAttributeName.Policy):
1✔
628
            policy = json.loads(policy)
1✔
629
            policy.setdefault("Statement", [])
1✔
630
        else:
631
            policy = {
1✔
632
                "Version": "2008-10-17",
633
                "Id": f"{self.arn}/SQSDefaultPolicy",
634
                "Statement": [],
635
            }
636
        policy.setdefault("Statement", [])
1✔
637
        existing_statement_ids = [statement.get("Sid") for statement in policy["Statement"]]
1✔
638
        if label in existing_statement_ids:
1✔
639
            raise InvalidParameterValueException(
1✔
640
                f"Value {label} for parameter Label is invalid. Reason: Already exists."
641
            )
642
        policy["Statement"].append(statement)
1✔
643
        self.attributes[QueueAttributeName.Policy] = json.dumps(policy)
1✔
644

645
    def remove_permission(self, label: str) -> None:
1✔
646
        """
647
        Delete a policy statement for usage of the remove_permission call
648

649
        :param label: Permission label
650
        """
651
        if policy := self.attributes.get(QueueAttributeName.Policy):
1✔
652
            policy = json.loads(policy)
1✔
653
            # this should not be necessary, but we can upload custom policies, so it's better to be safe
654
            policy.setdefault("Statement", [])
1✔
655
        else:
656
            policy = {
1✔
657
                "Version": "2008-10-17",
658
                "Id": f"{self.arn}/SQSDefaultPolicy",
659
                "Statement": [],
660
            }
661
        existing_statement_ids = [statement.get("Sid") for statement in policy["Statement"]]
1✔
662
        if label not in existing_statement_ids:
1✔
663
            raise InvalidParameterValueException(
1✔
664
                f"Value {label} for parameter Label is invalid. Reason: can't find label."
665
            )
666
        policy["Statement"] = [
1✔
667
            statement for statement in policy["Statement"] if statement.get("Sid") != label
668
        ]
669
        if policy["Statement"]:
1✔
670
            self.attributes[QueueAttributeName.Policy] = json.dumps(policy)
1✔
671
        else:
672
            del self.attributes[QueueAttributeName.Policy]
1✔
673

674
    def get_queue_attributes(self, attribute_names: AttributeNameList = None) -> dict[str, str]:
1✔
675
        if not attribute_names:
1✔
676
            return {}
1✔
677

678
        if QueueAttributeName.All in attribute_names:
1✔
679
            attribute_names = self.attributes.keys()
1✔
680

681
        result: Dict[QueueAttributeName, str] = {}
1✔
682

683
        for attr in attribute_names:
1✔
684
            try:
1✔
685
                getattr(QueueAttributeName, attr)
1✔
686
            except AttributeError:
1✔
687
                raise InvalidAttributeName(f"Unknown Attribute {attr}.")
1✔
688

689
            value = self.attributes.get(attr)
1✔
690
            if callable(value):
1✔
691
                func = value
1✔
692
                value = func()
1✔
693
                if value is not None:
1✔
694
                    result[attr] = value
1✔
695
            elif value == "False" or value == "True":
1✔
696
                result[attr] = value.lower()
1✔
697
            elif value is not None:
1✔
698
                result[attr] = value
1✔
699
        return result
1✔
700

701
    @staticmethod
1✔
702
    def remove_expired_messages_from_heap(
1✔
703
        heap: list[SqsMessage], message_retention_period: int
704
    ) -> list[SqsMessage]:
705
        """
706
        Removes from the given heap of SqsMessages all messages that have expired in the context of the current time
707
        and the given message retention period. The method manipulates the heap but retains the heap property.
708

709
        :param heap: an array satisfying the heap property
710
        :param message_retention_period: the message retention period to use in relation to the current time
711
        :return: a list of expired messages that have been removed from the heap
712
        """
713
        th = time.time() - message_retention_period
1✔
714

715
        expired = []
1✔
716
        while heap:
1✔
717
            # here we're leveraging the heap property "that a[0] is always its smallest element"
718
            # and the assumption that message.created == message.priority
719
            message = heap[0]
1✔
720
            if th < message.created:
1✔
721
                break
1✔
722
            # remove the expired element
723
            expired.append(message)
1✔
724
            heapq.heappop(heap)
1✔
725

726
        return expired
1✔
727

728
    def _pre_delete_checks(self, standard_message: SqsMessage, receipt_handle: str) -> None:
1✔
729
        """
730
        Runs any potential checks if a message that has been successfully identified via a receipt handle
731
        is indeed supposed to be deleted.
732
        For example, a receipt handle that has expired might not lead to deletion.
733

734
        :param standard_message: The message to be deleted
735
        :param receipt_handle: The handle associated with the message
736
        :return: None. Potential violations raise errors.
737
        """
738
        pass
1✔
739

740

741
class StandardQueue(SqsQueue):
1✔
742
    visible: InterruptiblePriorityQueue[SqsMessage]
1✔
743
    inflight: Set[SqsMessage]
1✔
744

745
    def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
1✔
746
        super().__init__(name, region, account_id, attributes, tags)
1✔
747
        self.visible = InterruptiblePriorityQueue()
1✔
748

749
    def clear(self):
1✔
750
        with self.mutex:
1✔
751
            super().clear()
1✔
752
            self.visible.queue.clear()
1✔
753

754
    @property
1✔
755
    def approx_number_of_messages(self):
1✔
756
        return self.visible.qsize()
1✔
757

758
    def shutdown(self):
1✔
759
        self.visible.shutdown()
1✔
760

761
    def put(
1✔
762
        self,
763
        message: Message,
764
        visibility_timeout: int = None,
765
        message_deduplication_id: str = None,
766
        message_group_id: str = None,
767
        delay_seconds: int = None,
768
    ):
769
        if message_deduplication_id:
1✔
770
            raise InvalidParameterValueException(
×
771
                f"Value {message_deduplication_id} for parameter MessageDeduplicationId is invalid. Reason: The "
772
                f"request includes a parameter that is not valid for this queue type."
773
            )
774

775
        standard_message = SqsMessage(time.time(), message)
1✔
776

777
        if visibility_timeout is not None:
1✔
778
            standard_message.visibility_timeout = visibility_timeout
×
779
        else:
780
            # use the attribute from the queue
781
            standard_message.visibility_timeout = self.visibility_timeout
1✔
782

783
        if delay_seconds is not None:
1✔
784
            standard_message.delay_seconds = delay_seconds
1✔
785
        else:
786
            standard_message.delay_seconds = self.delay_seconds
1✔
787

788
        if standard_message.is_delayed:
1✔
789
            self.delayed.add(standard_message)
1✔
790
        else:
791
            self._put_message(standard_message)
1✔
792

793
        return standard_message
1✔
794

795
    def _put_message(self, message: SqsMessage):
1✔
796
        self.visible.put_nowait(message)
1✔
797

798
    def remove_expired_messages(self):
1✔
799
        with self.mutex:
1✔
800
            messages = self.remove_expired_messages_from_heap(
1✔
801
                self.visible.queue, self.message_retention_period
802
            )
803

804
        for message in messages:
1✔
805
            LOG.debug("Removed expired message %s from queue %s", message.message_id, self.arn)
1✔
806

807
    def receive(
1✔
808
        self,
809
        num_messages: int = 1,
810
        wait_time_seconds: int = None,
811
        visibility_timeout: int = None,
812
        *,
813
        poll_empty_queue: bool = False,
814
    ) -> ReceiveMessageResult:
815
        result = ReceiveMessageResult()
1✔
816

817
        max_receive_count = self.max_receive_count
1✔
818
        visibility_timeout = (
1✔
819
            self.visibility_timeout if visibility_timeout is None else visibility_timeout
820
        )
821

822
        block = True if wait_time_seconds else False
1✔
823
        timeout = wait_time_seconds or 0
1✔
824
        start = time.time()
1✔
825

826
        # collect messages
827
        while True:
1✔
828
            try:
1✔
829
                message = self.visible.get(block=block, timeout=timeout)
1✔
830
            except Empty:
1✔
831
                break
1✔
832
            # setting block to false guarantees that, if we've already waited before, we don't wait the
833
            # full time again in the next iteration if max_number_of_messages is set but there are no more
834
            # messages in the queue. see https://github.com/localstack/localstack/issues/5824
835
            if not poll_empty_queue:
1✔
836
                block = False
1✔
837

838
            timeout -= time.time() - start
1✔
839
            if timeout < 0:
1✔
840
                timeout = 0
1✔
841

842
            if message.deleted:
1✔
843
                # filter messages that were deleted with an expired receipt handle after they have been
844
                # re-queued. this can only happen due to a race with `remove`.
845
                continue
×
846

847
            # update message attributes
848
            message.receive_count += 1
1✔
849
            message.update_visibility_timeout(visibility_timeout)
1✔
850
            message.set_last_received(time.time())
1✔
851
            if message.first_received is None:
1✔
852
                message.first_received = message.last_received
1✔
853

854
            LOG.debug("de-queued message %s from %s", message, self.arn)
1✔
855
            if max_receive_count and message.receive_count > max_receive_count:
1✔
856
                # the message needs to move to the DLQ
857
                LOG.debug(
1✔
858
                    "message %s has been received %d times, marking it for DLQ",
859
                    message,
860
                    message.receive_count,
861
                )
862
                result.dead_letter_messages.append(message)
1✔
863
            else:
864
                result.successful.append(message)
1✔
865
                message.increment_approximate_receive_count()
1✔
866

867
                # now we can return
868
                if len(result.successful) == num_messages:
1✔
869
                    break
1✔
870

871
        # now process the successful result messages: create receipt handles and manage visibility.
872
        for message in result.successful:
1✔
873
            # manage receipt handle
874
            receipt_handle = self.create_receipt_handle(message)
1✔
875
            message.receipt_handles.add(receipt_handle)
1✔
876
            self.receipts[receipt_handle] = message
1✔
877
            result.receipt_handles.append(receipt_handle)
1✔
878

879
            # manage message visibility
880
            if message.visibility_timeout == 0:
1✔
881
                self.visible.put_nowait(message)
1✔
882
            else:
883
                self.inflight.add(message)
1✔
884

885
        return result
1✔
886

887
    def _on_remove_message(self, message: SqsMessage):
1✔
888
        try:
1✔
889
            self.inflight.remove(message)
1✔
890
        except KeyError:
1✔
891
            # this likely means the message was removed with an expired receipt handle unfortunately this
892
            # means we need to scan the queue for the element and remove it from there, and then re-heapify
893
            # the queue
894
            try:
1✔
895
                self.visible.queue.remove(message)
1✔
896
                heapq.heapify(self.visible.queue)
1✔
897
            except ValueError:
1✔
898
                # this may happen if the message no longer exists because it was removed earlier
899
                pass
1✔
900

901
    def validate_queue_attributes(self, attributes):
1✔
902
        valid = [
1✔
903
            k[1]
904
            for k in inspect.getmembers(
905
                QueueAttributeName, lambda x: isinstance(x, str) and not x.startswith("__")
906
            )
907
            if k[1] not in sqs_constants.INVALID_STANDARD_QUEUE_ATTRIBUTES
908
        ]
909

910
        for k in attributes.keys():
1✔
911
            if k in [QueueAttributeName.FifoThroughputLimit, QueueAttributeName.DeduplicationScope]:
1✔
912
                raise InvalidAttributeName(
1✔
913
                    f"You can specify the {k} only when FifoQueue is set to true."
914
                )
915
            if k not in valid:
1✔
916
                raise InvalidAttributeName(f"Unknown Attribute {k}.")
1✔
917

918

919
class MessageGroup:
1✔
920
    message_group_id: str
1✔
921
    messages: list[SqsMessage]
1✔
922

923
    def __init__(self, message_group_id: str):
1✔
924
        self.message_group_id = message_group_id
1✔
925
        self.messages = []
1✔
926

927
    def empty(self) -> bool:
1✔
928
        return not self.messages
1✔
929

930
    def size(self) -> int:
1✔
931
        return len(self.messages)
×
932

933
    def pop(self) -> SqsMessage:
1✔
934
        return heapq.heappop(self.messages)
1✔
935

936
    def push(self, message: SqsMessage):
1✔
937
        heapq.heappush(self.messages, message)
1✔
938

939
    def __eq__(self, other):
1✔
940
        return self.message_group_id == other.message_group_id
×
941

942
    def __hash__(self):
1✔
943
        return self.message_group_id.__hash__()
1✔
944

945
    def __repr__(self):
946
        return f"MessageGroup(id={self.message_group_id}, size={len(self.messages)})"
947

948

949
class FifoQueue(SqsQueue):
1✔
950
    """
951
    A FIFO queue behaves differently than a default queue. Most behavior has to be implemented separately.
952

953
    See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html
954

955
    TODO: raise exceptions when trying to remove a message with an expired receipt handle
956
    """
957

958
    deduplication: Dict[str, SqsMessage]
1✔
959
    message_groups: dict[str, MessageGroup]
1✔
960
    inflight_groups: set[MessageGroup]
1✔
961
    message_group_queue: InterruptibleQueue
1✔
962
    deduplication_scope: str
1✔
963

964
    def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
1✔
965
        super().__init__(name, region, account_id, attributes, tags)
1✔
966
        self.deduplication = {}
1✔
967

968
        self.message_groups = {}
1✔
969
        self.inflight_groups = set()
1✔
970
        self.message_group_queue = InterruptibleQueue()
1✔
971

972
        # SQS does not seem to change the deduplication behaviour of fifo queues if you
973
        # change to/from 'queue'/'messageGroup' scope after creation -> we need to set this on creation
974
        self.deduplication_scope = self.attributes[QueueAttributeName.DeduplicationScope]
1✔
975

976
    @property
1✔
977
    def approx_number_of_messages(self):
1✔
978
        n = 0
1✔
979
        for message_group in self.message_groups.values():
1✔
980
            n += len(message_group.messages)
1✔
981
        return n
1✔
982

983
    def shutdown(self):
1✔
984
        self.message_group_queue.shutdown()
1✔
985

986
    def get_message_group(self, message_group_id: str) -> MessageGroup:
1✔
987
        """
988
        Thread safe lazy factory for MessageGroup objects.
989

990
        :param message_group_id: the message group ID
991
        :return: a new or existing MessageGroup object
992
        """
993
        with self.mutex:
1✔
994
            if message_group_id not in self.message_groups:
1✔
995
                self.message_groups[message_group_id] = MessageGroup(message_group_id)
1✔
996

997
            return self.message_groups.get(message_group_id)
1✔
998

999
    def default_attributes(self) -> QueueAttributeMap:
1✔
1000
        return {
1✔
1001
            **super().default_attributes(),
1002
            QueueAttributeName.ContentBasedDeduplication: "false",
1003
            QueueAttributeName.DeduplicationScope: "queue",
1004
            QueueAttributeName.FifoThroughputLimit: "perQueue",
1005
        }
1006

1007
    def update_delay_seconds(self, value: int):
1✔
1008
        super(FifoQueue, self).update_delay_seconds(value)
×
1009
        for message in self.delayed:
×
1010
            message.delay_seconds = value
×
1011

1012
    def _pre_delete_checks(self, message: SqsMessage, receipt_handle: str) -> None:
1✔
1013
        _, _, _, last_received = extract_receipt_handle_info(receipt_handle)
1✔
1014
        if time.time() - float(last_received) > message.visibility_timeout:
1✔
1015
            raise InvalidParameterValueException(
1✔
1016
                f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired."
1017
            )
1018

1019
    def remove(self, receipt_handle: str):
1✔
1020
        self.validate_receipt_handle(receipt_handle)
1✔
1021

1022
        super().remove(receipt_handle)
1✔
1023

1024
    def put(
1✔
1025
        self,
1026
        message: Message,
1027
        visibility_timeout: int = None,
1028
        message_deduplication_id: str = None,
1029
        message_group_id: str = None,
1030
        delay_seconds: int = None,
1031
    ):
1032
        if delay_seconds:
1✔
1033
            # in fifo queues, delay is only applied on queue level. However, explicitly setting delay_seconds=0 is valid
1034
            raise InvalidParameterValueException(
1✔
1035
                f"Value {delay_seconds} for parameter DelaySeconds is invalid. Reason: The request include parameter "
1036
                f"that is not valid for this queue type."
1037
            )
1038

1039
        if not message_group_id:
1✔
1040
            raise MissingRequiredParameterException(
1✔
1041
                "The request must contain the parameter MessageGroupId."
1042
            )
1043
        dedup_id = message_deduplication_id
1✔
1044
        content_based_deduplication = not is_message_deduplication_id_required(self)
1✔
1045
        if not dedup_id and content_based_deduplication:
1✔
1046
            dedup_id = hashlib.sha256(message.get("Body").encode("utf-8")).hexdigest()
1✔
1047
        if not dedup_id:
1✔
1048
            raise InvalidParameterValueException(
1✔
1049
                "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly"
1050
            )
1051

1052
        fifo_message = SqsMessage(
1✔
1053
            time.time(),
1054
            message,
1055
            message_deduplication_id=dedup_id,
1056
            message_group_id=message_group_id,
1057
            sequence_number=str(self.next_sequence_number()),
1058
        )
1059
        if visibility_timeout is not None:
1✔
1060
            fifo_message.visibility_timeout = visibility_timeout
×
1061
        else:
1062
            # use the attribute from the queue
1063
            fifo_message.visibility_timeout = self.visibility_timeout
1✔
1064

1065
        # FIFO queues always use the queue level setting for 'DelaySeconds'
1066
        fifo_message.delay_seconds = self.delay_seconds
1✔
1067

1068
        original_message = self.deduplication.get(dedup_id)
1✔
1069
        if (
1✔
1070
            original_message
1071
            and original_message.priority + sqs_constants.DEDUPLICATION_INTERVAL_IN_SEC
1072
            > fifo_message.priority
1073
            # account for deduplication scope required for (but not restricted to) high-throughput-mode
1074
            and (
1075
                not self.deduplication_scope == "messageGroup"
1076
                or fifo_message.message_group_id == original_message.message_group_id
1077
            )
1078
        ):
1079
            message["MessageId"] = original_message.message["MessageId"]
1✔
1080
        else:
1081
            if fifo_message.is_delayed:
1✔
1082
                self.delayed.add(fifo_message)
1✔
1083
            else:
1084
                self._put_message(fifo_message)
1✔
1085

1086
            self.deduplication[dedup_id] = fifo_message
1✔
1087

1088
        return fifo_message
1✔
1089

1090
    def _put_message(self, message: SqsMessage):
1✔
1091
        """Once a message becomes visible in a FIFO queue, its message group also becomes visible."""
1092
        message_group = self.get_message_group(message.message_group_id)
1✔
1093

1094
        with self.mutex:
1✔
1095
            previously_empty = message_group.empty()
1✔
1096
            # put the message into the group
1097
            message_group.push(message)
1✔
1098

1099
            # new messages should not make groups visible that are currently inflight
1100
            if message.receive_count < 1 and message_group in self.inflight_groups:
1✔
1101
                return
1✔
1102
            # if an older message becomes visible again in the queue, that message's group becomes visible also.
1103
            if message_group in self.inflight_groups:
1✔
1104
                self.inflight_groups.remove(message_group)
1✔
1105
                self.message_group_queue.put_nowait(message_group)
1✔
1106
            # if the group was previously empty, it was not yet added back to the queue
1107
            elif previously_empty:
1✔
1108
                self.message_group_queue.put_nowait(message_group)
1✔
1109

1110
    def remove_expired_messages(self):
1✔
1111
        with self.mutex:
1✔
1112
            retention_period = self.message_retention_period
1✔
1113
            for message_group in self.message_groups.values():
1✔
1114
                messages = self.remove_expired_messages_from_heap(
1✔
1115
                    message_group.messages, retention_period
1116
                )
1117

1118
                for message in messages:
1✔
1119
                    LOG.debug(
1✔
1120
                        "Removed expired message %s from message group %s in queue %s",
1121
                        message.message_id,
1122
                        message.message_group_id,
1123
                        self.arn,
1124
                    )
1125

1126
    def receive(
1✔
1127
        self,
1128
        num_messages: int = 1,
1129
        wait_time_seconds: int = None,
1130
        visibility_timeout: int = None,
1131
        *,
1132
        poll_empty_queue: bool = False,
1133
    ) -> ReceiveMessageResult:
1134
        """
1135
        Receive logic for FIFO queues is different from standard queues. See
1136
        https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-understanding-logic.html.
1137

1138
        When receiving messages from a FIFO queue with multiple message group IDs, SQS first attempts to
1139
        return as many messages with the same message group ID as possible. This allows other consumers to
1140
        process messages with a different message group ID. When you receive a message with a message group
1141
        ID, no more messages for the same message group ID are returned unless you delete the message, or it
1142
        becomes visible.
1143
        """
1144
        result = ReceiveMessageResult()
1✔
1145

1146
        max_receive_count = self.max_receive_count
1✔
1147
        visibility_timeout = (
1✔
1148
            self.visibility_timeout if visibility_timeout is None else visibility_timeout
1149
        )
1150

1151
        block = True if wait_time_seconds else False
1✔
1152
        timeout = wait_time_seconds or 0
1✔
1153
        start = time.time()
1✔
1154

1155
        received_groups: Set[MessageGroup] = set()
1✔
1156

1157
        # collect messages over potentially multiple groups
1158
        while True:
1✔
1159
            try:
1✔
1160
                group: MessageGroup = self.message_group_queue.get(block=block, timeout=timeout)
1✔
1161
            except Empty:
1✔
1162
                break
1✔
1163

1164
            if group.empty():
1✔
1165
                # this can be the case if all messages in the group are still invisible or
1166
                # if all messages of a group have been processed.
1167
                # TODO: it should be blocking until at least one message is in the queue, but we don't
1168
                #  want to block the group
1169
                # TODO: check behavior in case it happens if all messages were removed from a group due to message
1170
                #  retention period.
1171
                timeout -= time.time() - start
1✔
1172
                if timeout < 0:
1✔
1173
                    timeout = 0
1✔
1174
                continue
1✔
1175

1176
            self.inflight_groups.add(group)
1✔
1177

1178
            received_groups.add(group)
1✔
1179

1180
            if not poll_empty_queue:
1✔
1181
                block = False
1✔
1182

1183
            # we lock the queue while accessing the groups to not get into races with re-queueing/deleting
1184
            with self.mutex:
1✔
1185
                # collect messages from the group until a continue/break condition is met
1186
                while True:
1✔
1187
                    try:
1✔
1188
                        message = group.pop()
1✔
1189
                    except IndexError:
1✔
1190
                        break
1✔
1191

1192
                    if message.deleted:
1✔
1193
                        # this means the message was deleted with a receipt handle after its visibility
1194
                        # timeout expired and the messages was re-queued in the meantime.
UNCOV
1195
                        continue
×
1196

1197
                    # update message attributes
1198
                    message.receive_count += 1
1✔
1199
                    message.update_visibility_timeout(visibility_timeout)
1✔
1200
                    message.set_last_received(time.time())
1✔
1201
                    if message.first_received is None:
1✔
1202
                        message.first_received = message.last_received
1✔
1203

1204
                    LOG.debug("de-queued message %s from fifo queue %s", message, self.arn)
1✔
1205
                    if max_receive_count and message.receive_count > max_receive_count:
1✔
1206
                        # the message needs to move to the DLQ
1207
                        LOG.debug(
1✔
1208
                            "message %s has been received %d times, marking it for DLQ",
1209
                            message,
1210
                            message.receive_count,
1211
                        )
1212
                        result.dead_letter_messages.append(message)
1✔
1213
                    else:
1214
                        result.successful.append(message)
1✔
1215
                        message.increment_approximate_receive_count()
1✔
1216

1217
                        # now we can break the inner loop
1218
                        if len(result.successful) == num_messages:
1✔
1219
                            break
1✔
1220

1221
                # but we also need to check the condition to return from the outer loop
1222
                if len(result.successful) == num_messages:
1✔
1223
                    break
1✔
1224

1225
        # now process the successful result messages: create receipt handles and manage visibility.
1226
        # we use the mutex again because we are modifying the group
1227
        with self.mutex:
1✔
1228
            for message in result.successful:
1✔
1229
                # manage receipt handle
1230
                receipt_handle = self.create_receipt_handle(message)
1✔
1231
                message.receipt_handles.add(receipt_handle)
1✔
1232
                self.receipts[receipt_handle] = message
1✔
1233
                result.receipt_handles.append(receipt_handle)
1✔
1234

1235
                # manage message visibility
1236
                if message.visibility_timeout == 0:
1✔
1237
                    self._put_message(message)
1✔
1238
                else:
1239
                    self.inflight.add(message)
1✔
1240

1241
        return result
1✔
1242

1243
    def _on_remove_message(self, message: SqsMessage):
1✔
1244
        # if a message is deleted from the queue, the message's group can become visible again
1245
        message_group = self.get_message_group(message.message_group_id)
1✔
1246

1247
        with self.mutex:
1✔
1248
            try:
1✔
1249
                self.inflight.remove(message)
1✔
UNCOV
1250
            except KeyError:
×
1251
                # in FIFO queues, this should not happen, as expired receipt handles cannot be used to
1252
                # delete a message.
UNCOV
1253
                pass
×
1254
            self.update_message_group_visibility(message_group)
1✔
1255

1256
    def update_message_group_visibility(self, message_group: MessageGroup):
1✔
1257
        """
1258
        Check if the passed message group should be made visible again
1259
        """
1260

1261
        with self.mutex:
1✔
1262
            if message_group in self.inflight_groups:
1✔
1263
                # it becomes visible again only if there are no other in flight messages in that group
1264
                for message in self.inflight:
1✔
1265
                    if message.message_group_id == message_group.message_group_id:
1✔
1266
                        return
1✔
1267

1268
                self.inflight_groups.remove(message_group)
1✔
1269
                if not message_group.empty():
1✔
1270
                    self.message_group_queue.put_nowait(message_group)
1✔
1271

1272
    def _assert_queue_name(self, name):
1✔
1273
        if not name.endswith(".fifo"):
1✔
1274
            raise InvalidParameterValueException(
1✔
1275
                "The name of a FIFO queue can only include alphanumeric characters, hyphens, or underscores, "
1276
                "must end with .fifo suffix and be 1 to 80 in length"
1277
            )
1278
        # The .fifo suffix counts towards the 80-character queue name quota.
1279
        queue_name = name[:-5] + "_fifo"
1✔
1280
        super()._assert_queue_name(queue_name)
1✔
1281

1282
    def validate_queue_attributes(self, attributes):
1✔
1283
        valid = [
1✔
1284
            k[1]
1285
            for k in inspect.getmembers(QueueAttributeName)
1286
            if k not in sqs_constants.INTERNAL_QUEUE_ATTRIBUTES
1287
        ]
1288
        for k in attributes.keys():
1✔
1289
            if k not in valid:
1✔
UNCOV
1290
                raise InvalidAttributeName(f"Unknown Attribute {k}.")
×
1291
        # Special Cases
1292
        fifo = attributes.get(QueueAttributeName.FifoQueue)
1✔
1293
        if fifo and fifo.lower() != "true":
1✔
1294
            raise InvalidAttributeValue(
1✔
1295
                "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported."
1296
            )
1297

1298
    def next_sequence_number(self):
1✔
1299
        return next(global_message_sequence())
1✔
1300

1301
    def clear(self):
1✔
1302
        with self.mutex:
1✔
1303
            super().clear()
1✔
1304
            self.message_groups.clear()
1✔
1305
            self.inflight_groups.clear()
1✔
1306
            self.message_group_queue.queue.clear()
1✔
1307
            self.deduplication.clear()
1✔
1308

1309

1310
class SqsStore(BaseStore):
1✔
1311
    queues: Dict[str, SqsQueue] = LocalAttribute(default=dict)
1✔
1312

1313
    deleted: Dict[str, float] = LocalAttribute(default=dict)
1✔
1314

1315
    move_tasks: Dict[str, MessageMoveTask] = LocalAttribute(default=dict)
1✔
1316
    """Maps task IDs to their ``MoveMessageTask`` object. Task IDs can be found by decoding a task handle."""
1✔
1317

1318
    def expire_deleted(self):
1✔
1319
        for k in list(self.deleted.keys()):
1✔
1320
            if self.deleted[k] <= (time.time() - sqs_constants.RECENTLY_DELETED_TIMEOUT):
1✔
1321
                del self.deleted[k]
1✔
1322

1323

1324
sqs_stores = AccountRegionBundle("sqs", SqsStore)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc