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