-
Notifications
You must be signed in to change notification settings - Fork 352
feat: add health checks to proxy #859
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
Merged
Merged
Changes from 131 commits
Commits
Show all changes
142 commits
Select commit
Hold shift + click to select a range
b784246
Add basic /healthcheck directory structure
monazhn fe5a7bc
Initial commit of healthcheck
tifftoff d9b3840
Add README
monazhn fa57263
create function to initialize HTTP endpoints
tifftoff 0d5d6ae
add documentation for package healthcheck
tifftoff b03576a
add flag indicating whether to use healthcheck or not
tifftoff c0a954d
Add basic structure for liveness + readiness tests
monazhn c5fe04d
Initial commit
tifftoff c809c0b
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff 36388e3
add exported function for proxy client to sya its ready
tifftoff 30ea13e
make proxy succeed readiness test when ready for new connections
tifftoff 5c5e540
Add hc struct, max connections check for readiness
monazhn e77ecc8
Error check ListenAndServe
monazhn 405fbff
Add comments to healthcheck.go
monazhn b3836b6
set up testing
tifftoff d0be8f7
wrote tests for liveness, bad startup, and successful startup
tifftoff a31adb3
Include tests for Readiness and MaxConnections
monazhn c47bbc4
fixing tests
tifftoff 9a0328b
Revert code pushed by mistake :-(
monazhn 450f18f
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff d1ddfe5
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff b75abed
Change test for MaxConnections to be more complete
monazhn 200a92b
Remove unwanted test
monazhn d832fb7
Add License information
monazhn 10033a5
fix copyright year
tifftoff f14b4f2
use early returns instead of else
tifftoff 0b989c1
use early return on error case
tifftoff 234dd7a
sync reads and writes
tifftoff 599fd09
rename InitHealthCheck to NewHealthCheck
tifftoff ff2c39b
forgot an unlock
tifftoff 5835b2c
add pointer receiver to NotifyReadyForConnections()
tifftoff 1f8e64e
GoDoc for exported functions
monazhn 0efecc0
Keep reference to server
monazhn dfdfe35
Define consts for URLs, paths, port numbers
monazhn b3bc070
Close health check after each test
monazhn e6f929f
Fix for flaky tests.
monazhn 0dac6c5
"Factored" const declarations
monazhn 633f95c
Add informative error logs when readiness fails
monazhn 076329c
styling
tifftoff c7a4142
Merge branch 'GoogleCloudPlatform:main' into health-check
monazhn 5b1c3ab
Add comments
monazhn a8da1d6
renamed mutex variables, added comments
tifftoff 0ac0523
chagne Inc. to LLC
tifftoff e8b86b5
define locks in HC struct, replace usage of startupL with readinessL
tifftoff e2c8126
change CloseHealthCheck to Close
tifftoff 9ee6d99
omit newHealthCheck function
tifftoff b2cb592
edit test names
tifftoff 98bcf1c
added comments describing each test
tifftoff f98f04d
forgot a period
tifftoff 93e0a1f
removed variables storing the results of the health tests
tifftoff a5559bf
typo
tifftoff f8c1f97
Derive URLs from constants
monazhn 2d62d1e
Separate ListenAndServe into Listen and Serve
monazhn 9c519f7
made port number a configurable argument from command line
tifftoff 3a49c95
Use non-default ServeMux
monazhn 6bf83ea
Iterate readiness criteria
monazhn 463dd2e
Have Close function take a context as an argument
monazhn 46d226f
Define a constant for testing port
monazhn 8cbd3df
Remove deferred call to Close()
monazhn b6e2be3
rename flag to useHttpHealthCheck
tifftoff 502ee01
make caller ensure hc is not nil before NotifyReadyForConnections
tifftoff 32ed5c1
fix flag
tifftoff 306d070
Internal health check package
monazhn 6247e30
Remove incomplete README
monazhn 47088d6
Clean up comments
monazhn 33fb809
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
monazhn e629b61
change default port to 9090
tifftoff ddf3faa
place mutex before boolean
tifftoff 50c5156
removed newlines
tifftoff e0dae22
rename readinessTest and livenessTest to isReady and isLive
tifftoff 0bf5baf
rename NotifyReadyForConnections to NotifyStarted
tifftoff 0d7dc72
fix function names in comments
tifftoff de3bd1c
change package to healthcheck_test
tifftoff 331e6bc
inline
tifftoff 22729f3
reduce startedL critical section is isReady
tifftoff 8e2d75a
rename proxyClient parameter to c
tifftoff 2ca9230
make exported functions return errors instead of logging them
tifftoff 856eb58
use errors.Is instead of basic equality check
tifftoff 237410a
change MaxConnections from 10 to 1 when testing
tifftoff e5a5e41
Delete README file
monazhn fab7dd9
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
monazhn ae7b6ae
rename flag from useHttpHealthCheck to useHTTPHealthCheck
tifftoff 350e6cc
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff 2f19a54
Change port number for testing
monazhn 9043485
rename hcPort flag to healthCheckPort
tifftoff 115c201
Add GoDoc for started flag
monazhn 4734ed6
Add back deferred Close()
monazhn edb2a08
Rename HC to Server
monazhn c4cc7ec
Move internal pkg to cmd/cloud_sql_proxy/internal
monazhn 30e9c0b
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff 362ce6e
fix imports and name changes from HC to Server
tifftoff 8c58cc3
keep startedL and started together
tifftoff 07c3295
Rename health check port
monazhn 7931176
Merge branch 'health-check' of https://github.com/monazhn/cloudsql-pr…
tifftoff c795f3e
rename variables for clarity
tifftoff 6bcddc5
Check for concrete error type
monazhn 6f05d65
add AvailableConn function
tifftoff 3426cc9
Table-driven tests
monazhn b6ce6fe
Remove unnecessary newline characters
monazhn ac78dd0
Rename proxyClient to c
monazhn 3c84d6e
move started docstring
tifftoff 714ec3e
make Serve's log message more explicit
tifftoff 9533088
inline Close
tifftoff 84c0c85
make proxy exit if health check cannot be initialized
tifftoff ec6b649
Use pre-declared ctx variable
monazhn fc9f4da
use http package constants instead of 500 and 200
tifftoff fe19c6b
add comment to AvailableConn
tifftoff 3bc693c
use a channel for started instead of a bool
tifftoff 90002bf
change started channel type
tifftoff f40df79
Revert back to separate tests
monazhn ffa7280
use Once to ensure that started can only close once
tifftoff 4ba3cfe
make once a pointer
tifftoff 92af34a
Merge branch 'GoogleCloudPlatform:main' into health-check
monazhn 460f62c
Merge pull request #1 from monazhn/health-check
tifftoff aec9362
add startup probe
tifftoff 2048a33
add tests against /startup
tifftoff ef65fae
fix comments
tifftoff 14e6f04
add helper function that checks the status of the started channel
tifftoff 6751476
fix logging message typo
tifftoff 0055c15
forgot a return
tifftoff c5709e6
comments
tifftoff 57343df
Merge pull request #2 from monazhn/health-check
tifftoff c7d116a
Merge branch 'GoogleCloudPlatform:main' into main
monazhn 4834ab3
Merge branch 'GoogleCloudPlatform:main' into main
monazhn 11655a3
Add health check flags to README
monazhn dd22c2c
Clarify wording
monazhn c64a93c
Move changes into separate branch
monazhn ea94208
Add instructions for using the new health check feature (#3)
monazhn a29b40d
Merge branch 'GoogleCloudPlatform:main' into main
monazhn 046ffb4
Merge branch 'GoogleCloudPlatform:main' into main
monazhn 4aaff8d
Fix lint
monazhn 14c553c
Improve error message
monazhn 3e98064
Modify error condition
monazhn e1e8dfb
Remove region tags
monazhn f18f549
Use footer link
monazhn 1ab9c5c
Clean up docstrings
monazhn edef210
Clean up
monazhn e97040e
Add instructions for using health checks
monazhn 6b3e7dc
Update README.md
tifftoff 7bb5e81
Merge branch 'main' into main
monazhn 1339f8c
Link to README
monazhn 6ebdd83
Merge branch 'main' into main
enocom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // Copyright 2021 Google LLC All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package healthcheck tests and communicates the health of the Cloud SQL Auth proxy. | ||
| package healthcheck | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net" | ||
| "net/http" | ||
| "sync" | ||
|
|
||
| "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" | ||
| "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" | ||
| ) | ||
|
|
||
| const ( | ||
| startupPath = "/startup" | ||
| livenessPath = "/liveness" | ||
| readinessPath = "/readiness" | ||
| ) | ||
|
|
||
| // Server is a type used to implement health checks for the proxy. | ||
| type Server struct { | ||
| // started is used to indicate whether the proxy has finished starting up. | ||
| // If started is open, startup has not finished. If started is closed, | ||
| // startup is complete. | ||
| started chan struct{} | ||
| // once ensures that started can only be closed once. | ||
| once *sync.Once | ||
| // port designates the port number on which Server listens and serves. | ||
| port string | ||
| // srv is a pointer to the HTTP server used to communicated proxy health. | ||
monazhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| srv *http.Server | ||
| } | ||
|
|
||
| // NewServer initializes a Server object and exposes HTTP endpoints used to | ||
monazhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // communicate proxy health. | ||
| func NewServer(c *proxy.Client, port string) (*Server, error) { | ||
| mux := http.NewServeMux() | ||
|
|
||
| srv := &http.Server{ | ||
| Addr: ":" + port, | ||
| Handler: mux, | ||
| } | ||
|
|
||
| hcServer := &Server{ | ||
| started: make(chan struct{}), | ||
| once: &sync.Once{}, | ||
| port: port, | ||
| srv: srv, | ||
| } | ||
|
|
||
| mux.HandleFunc(startupPath, func(w http.ResponseWriter, _ *http.Request) { | ||
| if !hcServer.proxyStarted() { | ||
| w.WriteHeader(http.StatusServiceUnavailable) | ||
| w.Write([]byte("error")) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusOK) | ||
| w.Write([]byte("ok")) | ||
| }) | ||
|
|
||
| mux.HandleFunc(readinessPath, func(w http.ResponseWriter, _ *http.Request) { | ||
| if !isReady(c, hcServer) { | ||
| w.WriteHeader(http.StatusServiceUnavailable) | ||
| w.Write([]byte("error")) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusOK) | ||
| w.Write([]byte("ok")) | ||
| }) | ||
|
|
||
| mux.HandleFunc(livenessPath, func(w http.ResponseWriter, _ *http.Request) { | ||
| if !isLive() { // Because isLive() always returns true, this case should not be reached. | ||
| w.WriteHeader(http.StatusServiceUnavailable) | ||
| w.Write([]byte("error")) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusOK) | ||
| w.Write([]byte("ok")) | ||
| }) | ||
|
|
||
| ln, err := net.Listen("tcp", srv.Addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| go func() { | ||
| if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { | ||
| logging.Errorf("Failed to start health check HTTP server: %v", err) | ||
| } | ||
| }() | ||
|
|
||
| return hcServer, nil | ||
| } | ||
|
|
||
| // Close gracefully shuts down the HTTP server belonging to the Server object. | ||
monazhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func (s *Server) Close(ctx context.Context) error { | ||
| return s.srv.Shutdown(ctx) | ||
| } | ||
|
|
||
| // NotifyStarted tells the Server that the proxy has finished startup. | ||
| func (s *Server) NotifyStarted() { | ||
| s.once.Do(func() { close(s.started) }) | ||
| } | ||
|
|
||
| // proxyStarted returns true if started is closed, false otherwise. | ||
| func (s *Server) proxyStarted() bool { | ||
| select { | ||
| case <-s.started: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // isLive returns true as long as the proxy is running. | ||
| func isLive() bool { | ||
| return true | ||
| } | ||
|
|
||
| // isReady will check the following criteria before determining whether the | ||
| // proxy is ready for new connections. | ||
| // 1. Finished starting up / been sent the 'Ready for Connections' log. | ||
| // 2. Not yet hit the MaxConnections limit, if applicable. | ||
| func isReady(c *proxy.Client, s *Server) bool { | ||
| // Not ready until we reach the 'Ready for Connections' log | ||
| if !s.proxyStarted() { | ||
| logging.Errorf("Readiness failed because proxy has not finished starting up.") | ||
| return false | ||
| } | ||
|
|
||
| // Not ready if the proxy is at the optional MaxConnections limit. | ||
| if !c.AvailableConn() { | ||
| logging.Errorf("Readiness failed because proxy has reached the maximum connections limit (%d).", c.MaxConnections) | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
157 changes: 157 additions & 0 deletions
157
cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // Copyright 2021 Google LLC All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package healthcheck_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "syscall" | ||
| "testing" | ||
|
|
||
| "github.com/GoogleCloudPlatform/cloudsql-proxy/cmd/cloud_sql_proxy/internal/healthcheck" | ||
| "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" | ||
| ) | ||
|
|
||
| const ( | ||
| startupPath = "/startup" | ||
| livenessPath = "/liveness" | ||
| readinessPath = "/readiness" | ||
| testPort = "8090" | ||
| ) | ||
|
|
||
| // Test to verify that when the proxy client is up, the liveness endpoint writes http.StatusOK. | ||
| func TestLiveness(t *testing.T) { | ||
| s, err := healthcheck.NewServer(&proxy.Client{}, testPort) | ||
| if err != nil { | ||
| t.Fatalf("Could not initialize health check: %v", err) | ||
| } | ||
| defer s.Close(context.Background()) | ||
|
|
||
| resp, err := http.Get("http://localhost:" + testPort + livenessPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v", err) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| t.Errorf("Got status code %v instead of %v", resp.StatusCode, http.StatusOK) | ||
| } | ||
| } | ||
|
|
||
| // Test to verify that when startup has NOT finished, the startup and readiness endpoints write | ||
| // http.StatusServiceUnavailable. | ||
| func TestStartupFail(t *testing.T) { | ||
| s, err := healthcheck.NewServer(&proxy.Client{}, testPort) | ||
| if err != nil { | ||
| t.Fatalf("Could not initialize health check: %v\n", err) | ||
| } | ||
| defer s.Close(context.Background()) | ||
|
|
||
| resp, err := http.Get("http://localhost:" + testPort + startupPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v\n", err) | ||
| } | ||
| if resp.StatusCode != http.StatusServiceUnavailable { | ||
| t.Errorf("%v returned status code %v instead of %v", startupPath, resp.StatusCode, http.StatusServiceUnavailable) | ||
| } | ||
|
|
||
| resp, err = http.Get("http://localhost:" + testPort + readinessPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v\n", err) | ||
| } | ||
| if resp.StatusCode != http.StatusServiceUnavailable { | ||
| t.Errorf("%v returned status code %v instead of %v", readinessPath, resp.StatusCode, http.StatusServiceUnavailable) | ||
| } | ||
| } | ||
|
|
||
| // Test to verify that when startup HAS finished (and MaxConnections limit not specified), | ||
| // the startup and readiness endpoints write http.StatusOK. | ||
| func TestStartupPass(t *testing.T) { | ||
| s, err := healthcheck.NewServer(&proxy.Client{}, testPort) | ||
| if err != nil { | ||
| t.Fatalf("Could not initialize health check: %v\n", err) | ||
| } | ||
| defer s.Close(context.Background()) | ||
|
|
||
| // Simulate the proxy client completing startup. | ||
| s.NotifyStarted() | ||
|
|
||
| resp, err := http.Get("http://localhost:" + testPort + startupPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v\n", err) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| t.Errorf("%v returned status code %v instead of %v", startupPath, resp.StatusCode, http.StatusOK) | ||
| } | ||
|
|
||
| resp, err = http.Get("http://localhost:" + testPort + readinessPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v\n", err) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| t.Errorf("%v returned status code %v instead of %v", readinessPath, resp.StatusCode, http.StatusOK) | ||
| } | ||
| } | ||
|
|
||
| // Test to verify that when startup has finished, but MaxConnections has been reached, | ||
| // the readiness endpoint writes http.StatusServiceUnavailable. | ||
| func TestMaxConnectionsReached(t *testing.T) { | ||
| c := &proxy.Client{ | ||
| MaxConnections: 1, | ||
| } | ||
| s, err := healthcheck.NewServer(c, testPort) | ||
| if err != nil { | ||
| t.Fatalf("Could not initialize health check: %v", err) | ||
| } | ||
| defer s.Close(context.Background()) | ||
|
|
||
| s.NotifyStarted() | ||
| c.ConnectionsCounter = c.MaxConnections // Simulate reaching the limit for maximum number of connections | ||
|
|
||
| resp, err := http.Get("http://localhost:" + testPort + readinessPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v", err) | ||
| } | ||
| if resp.StatusCode != http.StatusServiceUnavailable { | ||
| t.Errorf("Got status code %v instead of %v", resp.StatusCode, http.StatusServiceUnavailable) | ||
| } | ||
| } | ||
|
|
||
| // Test to verify that after closing a healthcheck, its liveness endpoint serves | ||
| // an error. | ||
| func TestCloseHealthCheck(t *testing.T) { | ||
| s, err := healthcheck.NewServer(&proxy.Client{}, testPort) | ||
| if err != nil { | ||
| t.Fatalf("Could not initialize health check: %v", err) | ||
| } | ||
| defer s.Close(context.Background()) | ||
|
|
||
| resp, err := http.Get("http://localhost:" + testPort + livenessPath) | ||
| if err != nil { | ||
| t.Fatalf("HTTP GET failed: %v", err) | ||
| } | ||
| if resp.StatusCode != http.StatusOK { | ||
| t.Errorf("Got status code %v instead of %v", resp.StatusCode, http.StatusOK) | ||
| } | ||
|
|
||
| err = s.Close(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("Failed to close health check: %v", err) | ||
| } | ||
|
|
||
| _, err = http.Get("http://localhost:" + testPort + livenessPath) | ||
| if !errors.Is(err, syscall.ECONNREFUSED) { | ||
| t.Fatalf("HTTP GET did not give a 'connection refused' error after closing health check") | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.