mirror of https://github.com/grafana/loki
Fix delete updates (#6194)
* Fix delete updates * Update pkg/storage/stores/shipper/compactor/deletion/delete_requests_client.go Co-authored-by: Michel Hollands <42814411+MichelHollands@users.noreply.github.com> * Add compactor address to the storage config * review comments * Review feedback Co-authored-by: Michel Hollands <42814411+MichelHollands@users.noreply.github.com>pull/6267/head
parent
3b3fcf6c87
commit
9b2786bde3
@ -0,0 +1,174 @@ |
|||||||
|
package deletion |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/go-kit/log/level" |
||||||
|
|
||||||
|
"github.com/grafana/loki/pkg/util/log" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
orgHeaderKey = "X-Scope-OrgID" |
||||||
|
getDeletePath = "/loki/api/v1/delete" |
||||||
|
) |
||||||
|
|
||||||
|
type DeleteRequestsClient interface { |
||||||
|
GetAllDeleteRequestsForUser(ctx context.Context, userID string) ([]DeleteRequest, error) |
||||||
|
Stop() |
||||||
|
} |
||||||
|
|
||||||
|
type deleteRequestsClient struct { |
||||||
|
url string |
||||||
|
httpClient httpClient |
||||||
|
mu sync.RWMutex |
||||||
|
|
||||||
|
cache map[string][]DeleteRequest |
||||||
|
cacheDuration time.Duration |
||||||
|
|
||||||
|
stopChan chan struct{} |
||||||
|
} |
||||||
|
|
||||||
|
type httpClient interface { |
||||||
|
Do(*http.Request) (*http.Response, error) |
||||||
|
} |
||||||
|
|
||||||
|
type DeleteRequestsStoreOption func(c *deleteRequestsClient) |
||||||
|
|
||||||
|
func WithRequestClientCacheDuration(d time.Duration) DeleteRequestsStoreOption { |
||||||
|
return func(c *deleteRequestsClient) { |
||||||
|
c.cacheDuration = d |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func NewDeleteRequestsClient(addr string, c httpClient, opts ...DeleteRequestsStoreOption) (DeleteRequestsClient, error) { |
||||||
|
u, err := url.Parse(addr) |
||||||
|
if err != nil { |
||||||
|
level.Error(log.Logger).Log("msg", "error parsing url", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
u.Path = getDeletePath |
||||||
|
|
||||||
|
client := &deleteRequestsClient{ |
||||||
|
url: u.String(), |
||||||
|
httpClient: c, |
||||||
|
cacheDuration: 5 * time.Minute, |
||||||
|
cache: make(map[string][]DeleteRequest), |
||||||
|
stopChan: make(chan struct{}), |
||||||
|
} |
||||||
|
|
||||||
|
for _, o := range opts { |
||||||
|
o(client) |
||||||
|
} |
||||||
|
|
||||||
|
go client.updateLoop() |
||||||
|
return client, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) GetAllDeleteRequestsForUser(ctx context.Context, userID string) ([]DeleteRequest, error) { |
||||||
|
if cachedRequests, ok := c.getCachedRequests(userID); ok { |
||||||
|
return cachedRequests, nil |
||||||
|
} |
||||||
|
|
||||||
|
requests, err := c.getRequestsFromServer(ctx, userID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
c.mu.Lock() |
||||||
|
defer c.mu.Unlock() |
||||||
|
c.cache[userID] = requests |
||||||
|
|
||||||
|
return requests, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) getCachedRequests(userID string) ([]DeleteRequest, bool) { |
||||||
|
c.mu.RLock() |
||||||
|
defer c.mu.RUnlock() |
||||||
|
|
||||||
|
res, ok := c.cache[userID] |
||||||
|
return res, ok |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) Stop() { |
||||||
|
close(c.stopChan) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) updateLoop() { |
||||||
|
t := time.NewTicker(c.cacheDuration) |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-t.C: |
||||||
|
c.updateCache() |
||||||
|
case <-c.stopChan: |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) updateCache() { |
||||||
|
userIDs := c.currentUserIDs() |
||||||
|
|
||||||
|
newCache := make(map[string][]DeleteRequest) |
||||||
|
for _, userID := range userIDs { |
||||||
|
deleteReq, err := c.getRequestsFromServer(context.Background(), userID) |
||||||
|
if err != nil { |
||||||
|
level.Error(log.Logger).Log("msg", "error getting delete requests from the store", "err", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
newCache[userID] = deleteReq |
||||||
|
} |
||||||
|
|
||||||
|
c.mu.Lock() |
||||||
|
defer c.mu.Unlock() |
||||||
|
c.cache = newCache |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) currentUserIDs() []string { |
||||||
|
c.mu.RLock() |
||||||
|
defer c.mu.RUnlock() |
||||||
|
|
||||||
|
userIDs := make([]string, 0, len(c.cache)) |
||||||
|
for userID := range c.cache { |
||||||
|
userIDs = append(userIDs, userID) |
||||||
|
} |
||||||
|
|
||||||
|
return userIDs |
||||||
|
} |
||||||
|
|
||||||
|
func (c *deleteRequestsClient) getRequestsFromServer(ctx context.Context, userID string) ([]DeleteRequest, error) { |
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url, nil) |
||||||
|
if err != nil { |
||||||
|
level.Error(log.Logger).Log("msg", "error getting delete requests from the store", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
req.Header.Set(orgHeaderKey, userID) |
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req) |
||||||
|
if err != nil { |
||||||
|
level.Error(log.Logger).Log("msg", "error getting delete requests from the store", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if resp.StatusCode/100 != 2 { |
||||||
|
err := fmt.Errorf("unexpected status code: %d", resp.StatusCode) |
||||||
|
level.Error(log.Logger).Log("msg", "error getting delete requests from the store", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var deleteRequests []DeleteRequest |
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&deleteRequests); err != nil { |
||||||
|
level.Error(log.Logger).Log("msg", "error marshalling response", "err", err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return deleteRequests, nil |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
package deletion |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetCacheGenNumberForUser(t *testing.T) { |
||||||
|
t.Run("it requests results from the api", func(t *testing.T) { |
||||||
|
httpClient := &mockHTTPClient{ret: `[{"request_id":"test-request"}]`} |
||||||
|
client, err := NewDeleteRequestsClient("http://test-server", httpClient) |
||||||
|
require.Nil(t, err) |
||||||
|
|
||||||
|
deleteRequests, err := client.GetAllDeleteRequestsForUser(context.Background(), "userID") |
||||||
|
require.Nil(t, err) |
||||||
|
|
||||||
|
require.Len(t, deleteRequests, 1) |
||||||
|
require.Equal(t, "test-request", deleteRequests[0].RequestID) |
||||||
|
|
||||||
|
require.Equal(t, "http://test-server/loki/api/v1/delete", httpClient.req.URL.String()) |
||||||
|
require.Equal(t, http.MethodGet, httpClient.req.Method) |
||||||
|
require.Equal(t, "userID", httpClient.req.Header.Get("X-Scope-OrgID")) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("it caches the results", func(t *testing.T) { |
||||||
|
httpClient := &mockHTTPClient{ret: `[{"request_id":"test-request"}]`} |
||||||
|
client, err := NewDeleteRequestsClient("http://test-server", httpClient, WithRequestClientCacheDuration(100*time.Millisecond)) |
||||||
|
require.Nil(t, err) |
||||||
|
|
||||||
|
deleteRequests, err := client.GetAllDeleteRequestsForUser(context.Background(), "userID") |
||||||
|
require.Nil(t, err) |
||||||
|
require.Equal(t, "test-request", deleteRequests[0].RequestID) |
||||||
|
|
||||||
|
httpClient.ret = `[{"request_id":"different"}]` |
||||||
|
|
||||||
|
deleteRequests, err = client.GetAllDeleteRequestsForUser(context.Background(), "userID") |
||||||
|
require.Nil(t, err) |
||||||
|
require.Equal(t, "test-request", deleteRequests[0].RequestID) |
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond) |
||||||
|
|
||||||
|
deleteRequests, err = client.GetAllDeleteRequestsForUser(context.Background(), "userID") |
||||||
|
require.Nil(t, err) |
||||||
|
require.Equal(t, "different", deleteRequests[0].RequestID) |
||||||
|
|
||||||
|
client.Stop() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
type mockHTTPClient struct { |
||||||
|
ret string |
||||||
|
req *http.Request |
||||||
|
} |
||||||
|
|
||||||
|
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { |
||||||
|
c.req = req |
||||||
|
|
||||||
|
return &http.Response{ |
||||||
|
StatusCode: 200, |
||||||
|
Body: io.NopCloser(strings.NewReader(c.ret)), |
||||||
|
}, nil |
||||||
|
} |
||||||
Loading…
Reference in new issue