Skip to content

Fix bugs found in deep code review (v7.3.0) - #37

Merged
jamesabel merged 3 commits into
mainfrom
fix/deep-code-review
Jul 4, 2026
Merged

Fix bugs found in deep code review (v7.3.0)#37
jamesabel merged 3 commits into
mainfrom
fix/deep-code-review

Conversation

@jamesabel

Copy link
Copy Markdown
Owner

Summary

Fixes ~30 issues found in a deep code review of the whole library, spanning correctness bugs, error handling, concurrency, and resource lifecycle. Full test suite passes (91/91, moto mock) and mypy is clean.

Correctness

  • dynamodb: upsert_item built an invalid UpdateExpression for multi-attribute items (missing commas) and failed on DynamoDB reserved words; now uses ExpressionAttributeNames/Values placeholders. Exceptions that were constructed but never raised (upsert_item(item=None), cache eviction) are now raised. scan_table no longer returns partial results on connection errors. Metadata-table mtime updates auto-create the metadata table (as documented) and are guarded consistently.
  • s3: upload change-detection compared against get_sha512(), which never returns None, so non-awsimple objects always re-uploaded and the documented mtime fallback was dead code; now compares the raw .sha512. upload/download raise AWSimpleException after exhausting retries instead of silently returning False (so download_cached can no longer cache a failed download as success). download accepts str paths as documented. get_s3_object_url handles us-east-1 and uses the modern s3.{region} endpoint. bucket_exists honors the configured session/profile/LocalStack endpoint. get_s3_object_metadata uses a single head_object (dir() was ~3 API calls per object).
  • dynamodb_miv: put_item bypassed metadata mtime updates (stale scan_table_cached results) and could silently overwrite items under concurrent writes; now updates metadata and uses a conditional put with retries.
  • pubsub: old-queue cleanup could never match a queue (hash-prefix mismatch) — queue names are now channel-prefixed; make_name_aws_safe joins args with a separator so ("a","b") no longer collides with ("ab",). Main loop drains message queues each cycle (bursts previously throttled to ~1 message per 10 s). Threads survive transient AWS/JSON errors and log fatal failures instead of dying silently; module uses getLogger.
  • sqs/sns: boto_error_to_string no longer crashes on BotoCoreError subclasses (the long-poll error handler crashed on the exact errors it was written to survive). Queue policies are merged rather than overwritten (subscribing to a second SNS topic no longer revokes the first). get_all_sqs_queues passes credentials through.
  • aws: moto mock lifecycle is reference counted process-wide (env-var save/restore was GC-order dependent). test() calls STS GetCallerIdentity (the old check was offline-only, validated nothing, and raised PermissionError for services like logs); returns False on invalid credentials. LocalStack + resource_name=None no longer leaves .resource unset.
  • logs: retries once with the expected sequence token instead of dropping the message.

Resource lifecycle / API polish

  • functools.lru_cache/cache on instance methods (dynamodb, sns) pinned every instance for the process lifetime; replaced with per-instance caches.
  • Single exception root: everything derives from AWSimpleException.
  • dict_to_dynamodb preserves zero-length strings (supported by DynamoDB since May 2020).
  • create_table key types are str/int/bytes; bool is rejected (DynamoDB "B" is binary).
  • LRU cache eviction skips directories and measures free space on the cache directory's volume.

Behavior changes to note

  • upload()/download() raise AWSimpleException on persistent failure instead of returning False.
  • test() returns False for invalid credentials instead of passing an offline check.
  • Pub/sub queue names changed format; stale queues from the old format age out via the now-functional remove_old_queues.
  • dict_to_dynamodb("") returns "" instead of None.

Tests

  • conftest holds a session-scoped AWSAccess so moto state persists across the suite deliberately (previously an accident of the lru_cache leak).
  • scan_table_cached stamps its cache file with time.time(), fixing a Windows clock-granularity flake with only a ~5 ms natural margin.
  • pytest scoped to test_awsimple via pyproject.toml so bare pytest doesn't execute examples/*_test.py at import time.
  • Version bumped to 7.3.0.

🤖 Generated with Claude Code

jamesabel and others added 3 commits July 3, 2026 17:19
Correctness fixes:
- dynamodb: upsert_item built an invalid UpdateExpression for multi-attribute
  items (missing commas) and failed on reserved words; now uses
  ExpressionAttributeNames/Values placeholders. Raise (not just construct)
  AWSimpleException for item=None (same fix in cache.py eviction).
- s3: upload()/upload_object_as_json() compared against get_sha512(), which
  never returns None (it synthesizes a substitute hash), so non-awsimple
  objects always re-uploaded and the mtime fallback was dead code; compare
  the raw .sha512 field instead. upload/download now raise AWSimpleException
  after exhausting retries instead of silently returning False, so
  download_cached no longer caches a failed download as success.
  download() converts str dest_path instead of asserting on it.
- dynamodb: scan_table no longer swallows connection errors mid-pagination
  (returned partial results as if complete, poisoning the pickle cache).
- dynamodb_miv: put_item now updates the metadata-table mtime (cache
  invalidation was bypassed) and uses a conditional put with retries so
  concurrent writers can't silently overwrite the same miv.
- pubsub: queue names now use the channel as a prefix so remove_old_queues()
  can actually find them (previously a hash mismatch made cleanup a no-op);
  make_name_aws_safe joins args with a separator so ("a","b") no longer
  collides with ("ab",). Main loop drains the pub/sub queues each cycle
  (was ~1 message per 10s under burst). Threads log-and-survive transient
  AWS/JSON errors instead of dying silently; use getLogger instead of a
  raw Logger instance.
- sqs/aws: boto_error_to_string no longer crashes on BotoCoreError subclasses
  (HTTPClientError has no .response). get_all_sqs_queues passes credentials
  through. add_permission and _connect_sns_to_sqs merge into the existing
  queue policy instead of overwriting it.
- aws: moto mock lifecycle is now reference counted process-wide (env vars
  restored correctly regardless of GC order). test() calls STS
  GetCallerIdentity (get_available_resources was offline-only and raised
  PermissionError for services without boto3 resources, e.g. logs).
  LocalStack + resource_name=None no longer leaves .resource unset.
- dynamodb/sns: replace lru_cache/cache on instance methods (leaked every
  instance for the process lifetime) with per-instance caches.
- dynamodb: metadata-table mtime update auto-creates the metadata table as
  documented; delete_item/upsert_item/delete_all_items guard a None
  metadata_table; scan_table_cached only unpickles on an actual cache hit
  and stamps the cache file with time.time() so Windows clock-tick lag
  can't make it appear older than the metadata mtime.
- s3: get_s3_object_url handles us-east-1 (LocationConstraint None) and uses
  the current s3.{region} endpoint style; bucket location cached per
  instance. get_s3_object_metadata uses a single head_object call (dir()
  previously made ~3 API calls per object). bucket_exists uses the
  configured session/localstack endpoint instead of a fresh default client.
- logs: on InvalidSequenceTokenException retry once with the expected token
  instead of dropping the message; fallback file opens in append mode.
- dynamodb: dict_to_dynamodb keeps zero-length strings (supported by
  DynamoDB since May 2020); create_table key types are str/int/bytes and
  bool is rejected ("B" is Binary, not boolean).
- cache: LRU eviction skips directories; free-disk check measures the cache
  directory's volume; SQS response history uses a JSON-safe sentinel key and
  guards against an empty history.
- exceptions: single root exception (AWSimpleException) for all awsimple
  errors; mock.py no longer freezes env-var reads with @cache.

Tests: conftest holds a session-scoped AWSAccess so moto state persists
across the suite (previously an accident of the lru_cache leak);
test_pubsub_list_queues creates its own queue; updated assertions that
codified the empty-string and name-collision bugs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_awsimple

The STS-based test() raised a raw ClientError (e.g. InvalidClientTokenId)
for invalid credentials, which broke examples/aws_access_test.py - notably
under bare pytest, which collected examples/*_test.py and executed the
example at import time. test() now returns False for ClientError /
NoCredentialsError (setting most_recent_error) while configuration errors
like ProfileNotFound still raise, and pytest is scoped to test_awsimple via
pyproject testpaths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jamesabel
jamesabel merged commit 216ece0 into main Jul 4, 2026
3 checks passed
@jamesabel
jamesabel deleted the fix/deep-code-review branch July 4, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant