Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧹 chore: Improve Performance of Fiber Router #3261

Merged
merged 14 commits into from
Dec 29, 2024
Merged

Conversation

gaby
Copy link
Member

@gaby gaby commented Dec 21, 2024

Description

  • Micro performance improvements for the Fiber Router.
  • Refactored requestHandler into two methods. One for CustomCtx and one for DefaultCtx.

Improvements to RemoveEscapeChar()

After:

ubuntu@ubuntu:~/Desktop/git/fiber$ go test -benchmem -run=^$ -bench ^Benchmark_Utils_RemoveEscapeChar$ github.com/gofiber/fiber/v3 -count=4
goos: linux
goarch: amd64
pkg: github.com/gofiber/fiber/v3
cpu: AMD Ryzen 7 7800X3D 8-Core Processor           
Benchmark_Utils_RemoveEscapeChar-4   	48737022	        21.87 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	51360970	        21.88 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	53682570	        21.97 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	45811604	        21.91 ns/op	      16 B/op	       1 allocs/op
PASS
ok  	github.com/gofiber/fiber/v3	4.494s

Before:

ubuntu@ubuntu:~/Desktop/git/fiber$ go test -benchmem -run=^$ -bench ^Benchmark_Utils_RemoveEscapeChar$ github.com/gofiber/fiber/v3 -count=4
goos: linux
goarch: amd64
pkg: github.com/gofiber/fiber/v3
cpu: AMD Ryzen 7 7800X3D 8-Core Processor           
Benchmark_Utils_RemoveEscapeChar-4   	34108017	        33.72 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	31172214	        33.48 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	34733971	        33.61 ns/op	      16 B/op	       1 allocs/op
Benchmark_Utils_RemoveEscapeChar-4   	34788264	        33.87 ns/op	      16 B/op	       1 allocs/op
PASS
ok  	github.com/gofiber/fiber/v3	4.707s

Router Improvements

Benchmark between v2, main, and this PR

image

Type of change

  • Performance improvement (non-breaking change which improves efficiency)

Copy link
Contributor

coderabbitai bot commented Dec 21, 2024

Walkthrough

This pull request encompasses multiple changes across different files in the Fiber framework. The modifications include updating the golangci-lint version in the Makefile, enhancing routing and request handling logic in router.go, adding a Drop() method to the Ctx interface, optimizing the RemoveEscapeChar function in path.go, and introducing a new parallel benchmark in router_test.go. The changes focus on improving performance, flexibility, and maintainability of the framework's core components.

Changes

File Change Summary
Makefile Updated golangci-lint version from v1.62.0 to v1.62.2
app.go Modified NewCtxFunc and init methods to handle custom request handlers more flexibly
binder/mapping.go Moved a linter comment directive to the same line as the switch statement
ctx_interface_gen.go Added new Drop() method to Ctx interface for closing connections without sending response
path.go Optimized RemoveEscapeChar function with more efficient single-pass implementation
router.go Refactored routing logic, split request handler, improved path matching and context handling
router_test.go Added Benchmark_Router_Next_Default_Parallel for performance testing
app_test.go Added tests for handling custom HTTP methods "JANE" and "JOHN"
ctx_test.go Added test for custom context handling with new method "JOHN"

Possibly Related PRs

Suggested Reviewers

  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 Lint's version bumped with care,
Routing logic now more fair.
Connections drop, performance soar,
Fiber's magic opens a new door.
Code evolves with rabbit's flair! 🚀

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Dec 21, 2024

Codecov Report

Attention: Patch coverage is 79.00000% with 21 lines in your changes missing coverage. Please review.

Project coverage is 84.23%. Comparing base (775e0a7) to head (14a8635).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
router.go 80.51% 10 Missing and 5 partials ⚠️
app.go 57.14% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3261      +/-   ##
==========================================
- Coverage   84.42%   84.23%   -0.19%     
==========================================
  Files         116      116              
  Lines       11497    11519      +22     
==========================================
- Hits         9706     9703       -3     
- Misses       1374     1391      +17     
- Partials      417      425       +8     
Flag Coverage Δ
unittests 84.23% <79.00%> (-0.19%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

path.go Show resolved Hide resolved
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 14a8635 Previous: 775e0a7 Ratio
`Benchmark_RoutePatternMatch//api/:param/fixedEnd_ not_match _/api/abc/def/fixedEnd - allocs/op` 14 allocs/op
BenchmarkUnmarshalitem-4_middleware_limiter 40.61 ns/op 640.26 MB/s 0 B/op 0 allocs/op 26.65 ns/op 975.75 MB/s 0 B/op 0 allocs/op 1.52
BenchmarkUnmarshalitem-4_middleware_limiter - ns/op 40.61 ns/op 26.65 ns/op 1.52

This comment was automatically generated by workflow using github-action-benchmark.

@gaby gaby marked this pull request as ready for review December 28, 2024 05:32
@gaby gaby requested a review from a team as a code owner December 28, 2024 05:32
@gaby gaby requested review from sixcolors, ReneWerner87 and efectn and removed request for a team December 28, 2024 05:32
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
app.go (2)

619-622: Tests recommended for dynamic NewCtxFunc changes
By setting app.server.Handler to app.customRequestHandler on non-nil newCtxFunc, the server’s behavior changes significantly. Consider adding tests that confirm correct fallback to defaultRequestHandler when newCtxFunc is nil.


875-879: Handler returns custom or default
This block selects between app.customRequestHandler and app.defaultRequestHandler. Looks functionally correct. Adding minimal test coverage ensures stable switching whenever newCtxFunc is set or unset.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 877-878: app.go#L877-L878
Added lines #L877 - L878 were not covered by tests

ctx_interface_gen.go (1)

353-355: New Drop() method
The Drop() method can be valuable for quietly terminating connections (like DDoS mitigation). Ensure calling it doesn’t break existing middlewares that assume a response is always sent.

Could you document how or when this should be invoked in high-load cases?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57744eb and 8e213e0.

⛔ Files ignored due to path filters (1)
  • .github/workflows/linter.yml is excluded by !**/*.yml
📒 Files selected for processing (7)
  • Makefile (1 hunks)
  • app.go (3 hunks)
  • binder/mapping.go (1 hunks)
  • ctx_interface_gen.go (1 hunks)
  • path.go (1 hunks)
  • router.go (4 hunks)
  • router_test.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • binder/mapping.go
🧰 Additional context used
🪛 GitHub Check: codecov/patch
app.go

[warning] 877-878: app.go#L877-L878
Added lines #L877 - L878 were not covered by tests


[warning] 1069-1069: app.go#L1069
Added line #L1069 was not covered by tests

router.go

[warning] 218-218: router.go#L218
Added line #L218 was not covered by tests


[warning] 232-233: router.go#L232-L233
Added lines #L232 - L233 were not covered by tests


[warning] 249-249: router.go#L249
Added line #L249 was not covered by tests


[warning] 270-270: router.go#L270
Added line #L270 was not covered by tests

🔇 Additional comments (8)
router.go (4)

214-243: Add test coverage for edge conditions
Lines #218 (panic) and #232–233 (flash cookie handling) appear uncovered by tests. Consider adding unit or integration tests to validate the behavior when the context pool fails to provide a *DefaultCtx and when request headers include flash cookies.

Do you want help generating a test function for these lines?

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 218-218: router.go#L218
Added line #L218 was not covered by tests


[warning] 232-233: router.go#L232-L233
Added lines #L232 - L233 were not covered by tests


245-270: Extend coverage for custom request handling
Similar to the default handler, lines #249 (panic) and #270 (internal server error state) lack explicit test coverage. Ensure that custom context scenarios are tested to guarantee stability and maintain confidence in your customRequestHandler.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 249-249: router.go#L249
Added line #L249 was not covered by tests


[warning] 270-270: router.go#L270
Added line #L270 was not covered by tests


327-376: Registration logic looks good
The path normalization and route registration improvements appear consistent. The single-pass path cleaning and constraints for empty handlers (panics) make maintenance easier. Great work!


68-106: Consider verifying edge cases in route matching
This logic correctly checks for root (r.root) and r.star. However, please ensure that complex edge cases (e.g., detectionPath unexpectedly empty or partial prefix matches) are handled as intended, especially when r.use is true.

Run this script to search relevant tests covering Route.match logic:

✅ Verification successful

Let me check the test implementations to verify the edge cases.


Route matching logic is well-tested for edge cases
The test suite comprehensively covers all edge cases in the route matching logic:

  • Empty paths and root handling (Test_Route_Match_Root)
  • Wildcard/star patterns (Test_Route_Match_Star)
  • Middleware prefix matches (Test_Route_Match_Middleware_HasPrefix)
  • Parameter parsing (Test_Route_Match_Parser)
  • Special character handling (Test_Route_Match_UnescapedPath, Test_Route_Match_WithEscapeChar)
  • Root middleware behavior (Test_Route_Match_Middleware_Root)

The implementation correctly handles all these cases, including empty paths, partial matches, and special characters.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# This script looks for test methods covering `Route.match` usage
rg -A 8 "func Test_Route_Match" 

Length of output: 2847


Script:

#!/bin/bash
# Get the full test implementations
rg -B 2 -A 25 "^func Test_Route_Match" router_test.go

Length of output: 6582

app.go (1)

1068-1072: Initialize the correct request handler
This chunk replicates the logic above, bridging the custom or default handler based on newCtxFunc. Ensuring consistency between Handler() and init() is key. Good design.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1069-1069: app.go#L1069
Added line #L1069 was not covered by tests

router_test.go (1)

594-615: Parallel benchmark is a great addition
The new function Benchmark_Router_Next_Default_Parallel significantly broadens performance testing under concurrency. Consider adding testcases for heavier routes or dynamic handlers to measure real-world usage.

path.go (1)

623-632: Impressive single-pass removal
Shifting to a manual byte-slice approach for escaping yields meaningful performance gains. The logic is straightforward, and skipping copy operations should reduce allocations.

Makefile (1)

38-38: LGTM! Verify the golangci-lint version adoption.

The minor version bump of golangci-lint is a good maintenance practice. Let's verify the version's stability and adoption rate.

✅ Verification successful

Golangci-lint version update is safe to proceed

The verification shows:

  • The version v1.62.2 is only referenced in the Makefile, indicating this is a new adoption
  • The GitHub workflow uses the golangci-lint GitHub action v6, which is version-agnostic and compatible with the CLI version being used
  • No specific version constraints or issues were found in the documentation

Since this is a patch version update (v1.62.0 → v1.62.2) and the tooling setup is properly maintained through both Makefile and GitHub Actions, this change is safe to proceed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the adoption rate and stability of golangci-lint v1.62.2

# Check if this version is referenced in go.mod files across the codebase
rg -l "golangci-lint.*v1.62.2" 

# Look for any version-specific comments or issues mentioned in docs
fd -t f -e md | xargs rg "golangci-lint.*1\.62\.2"

# Check if there are any version constraints in GitHub workflows
fd -t f -g "*.yml" -g "*.yaml" .github/workflows/ | xargs rg "golangci-lint.*@v"

Length of output: 306

@gaby gaby added this to the v3 milestone Dec 28, 2024
router.go Show resolved Hide resolved
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
router.go (2)

214-243: Well-structured handler split with room for optimization

The separation into defaultRequestHandler and customRequestHandler improves type safety and performance. Consider extracting common logic (flash message handling, error handling) into helper functions to reduce duplication.

+ func (app *App) handleFlashMessages(req *fasthttp.Request) bool {
+   rawHeaders := req.Header.RawHeaders()
+   return len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName))
+ }
+
+ func (app *App) handleError(ctx any, err error) {
+   if catch := ctx.(interface{ App() *App }).App().ErrorHandler(ctx, err); catch != nil {
+     _ = ctx.(interface{ SendStatus(int) error }).SendStatus(StatusInternalServerError)
+   }
+ }

Also applies to: 245-273

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 218-218: router.go#L218
Added line #L218 was not covered by tests


[warning] 232-232: router.go#L232
Added line #L232 was not covered by tests


343-376: Consider validating HTTP method earlier

The route registration logic is solid, but method validation could be moved before the expensive path parsing operations to fail fast on invalid methods.

- parsedRaw := parseRoute(pathRaw, app.customConstraints...)
- parsedPretty := parseRoute(pathPretty, app.customConstraints...)
-
  for _, method := range methods {
    method = utils.ToUpper(method)
    if method != methodUse && app.methodInt(method) == -1 {
      panic(fmt.Sprintf("add: invalid http method %s\n", method))
    }
+   
+   parsedRaw := parseRoute(pathRaw, app.customConstraints...)
+   parsedPretty := parseRoute(pathPretty, app.customConstraints...)
app_test.go (1)

617-642: Refactor to unify tests for custom methods
The test logic in Test_App_All_Method_Test mirrors the approach in Test_App_Add_Method_Test. While this is fully functional, consider consolidating or parameterizing these tests to reduce duplication and streamline maintenance of custom method tests.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e213e0 and 2e5ef2c.

📒 Files selected for processing (2)
  • app_test.go (1 hunks)
  • router.go (4 hunks)
🧰 Additional context used
🪛 GitHub Check: codecov/patch
router.go

[warning] 218-218: router.go#L218
Added line #L218 was not covered by tests


[warning] 232-232: router.go#L232
Added line #L232 was not covered by tests


[warning] 249-249: router.go#L249
Added line #L249 was not covered by tests


[warning] 256-258: router.go#L256-L258
Added lines #L256 - L258 were not covered by tests


[warning] 263-264: router.go#L263-L264
Added lines #L263 - L264 were not covered by tests


[warning] 269-270: router.go#L269-L270
Added lines #L269 - L270 were not covered by tests

🔇 Additional comments (5)
router.go (4)

68-106: LGTM! Route matching optimizations look solid

The optimizations to the route matching logic are well-implemented:

  • More efficient root path detection using length check
  • Clear separation of wildcard, parameter, and middleware matching logic
  • Improved middleware route handling with explicit root path cases

218-218: Add test coverage for error paths

Several critical code paths lack test coverage:

  • Type assertion failures
  • Invalid HTTP method handling
  • Flash message processing
  • Error handling scenarios

Please add test cases to cover these scenarios to ensure reliability.

Would you like me to help generate comprehensive test cases for these paths?

Also applies to: 232-232, 249-249, 256-258, 263-264, 269-270

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 218-218: router.go#L218
Added line #L218 was not covered by tests


327-342: LGTM! Efficient path normalization implementation

The path normalization logic is well-structured and efficient:

  • Precomputes normalization once
  • Handles all edge cases (empty paths, leading/trailing slashes)
  • Respects configuration settings (case sensitivity, strict routing)

255-255: Verify methodInt implementation as mentioned

As noted in the previous review, there might be a bug with app.methodInt(). Please verify the implementation and consider adding a check.

app_test.go (1)

609-616: Add clarity on custom method naming
These lines introduce a new custom method "JANE" for testing. The logic correctly verifies the method’s handling. If you intend to keep "JANE" for broader usage, consider documenting in the Fiber or project-specific docs how custom methods are added and recognized, so that it's easy for developers to discover and maintain.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
app_test.go (2)

584-608: LGTM! Well-structured test for custom HTTP method handling.

The test comprehensively covers:

  • Adding custom method "JOHN"
  • Successful request handling (200 OK)
  • Method not allowed case (405)
  • Not implemented case (501)
  • Panic behavior for invalid method addition

Consider adding test cases for:

  • Multiple custom methods in a single Add() call
  • Case sensitivity validation
  • Special characters in method names

Line range hint 610-635: LGTM! Good coverage of All() method with custom HTTP methods.

The test effectively validates:

  • All() method with custom method "JOHN"
  • Adding new method "JANE" after All()
  • Status code validation

Consider adding test cases for:

  • Concurrent requests with multiple methods
  • Error handling for malformed requests
  • Header validation in responses
ctx_test.go (1)

130-157: LGTM! Good test coverage for custom context with custom methods.

The test effectively validates:

  • Custom context creation with custom method
  • Request handling with custom method "JOHN"
  • Panic behavior for invalid method "JANE"

Consider adding test cases for:

  • Context behavior with middleware chain
  • Error handling in custom context
  • Memory leaks validation with deferred cleanup
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5ef2c and cd0b631.

📒 Files selected for processing (2)
  • app_test.go (2 hunks)
  • ctx_test.go (1 hunks)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
app_test.go (1)

610-628: Consider adding test cases for standard HTTP methods.

While the test covers custom method handling, it would be beneficial to verify that the All route responds correctly to standard HTTP methods (GET, POST, etc.) as well.

Add test cases to verify standard HTTP methods:

 app.All("/doe", testEmptyHandler)

 resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
 require.NoError(t, err, "app.Test(req)")
 require.Equal(t, StatusOK, resp.StatusCode, "Status code")

+// Test standard HTTP methods
+for _, method := range []string{MethodGet, MethodPost, MethodPut, MethodDelete} {
+    resp, err := app.Test(httptest.NewRequest(method, "/doe", nil))
+    require.NoError(t, err, "app.Test(req)")
+    require.Equal(t, StatusOK, resp.StatusCode, fmt.Sprintf("Status code for %s", method))
+}

 // Add a new method
 require.Panics(t, func() {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd0b631 and 14a8635.

📒 Files selected for processing (1)
  • app_test.go (1 hunks)
🔇 Additional comments (1)
app_test.go (1)

590-608: LGTM! Well-structured test cases for custom method handling.

The test comprehensively covers:

  • Success case with custom method
  • Method not allowed case
  • Unknown method case
  • Panic case when adding method after initialization

@ReneWerner87 ReneWerner87 merged commit 845a7f8 into main Dec 29, 2024
11 of 14 checks passed
@gaby gaby deleted the router-improvements branch December 29, 2024 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants