1
This commit is contained in:
313
internal/integration/client.go
Normal file
313
internal/integration/client.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
|
||||
cache2 "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database"
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
HandleVacancyResponse(request HandleVacancy) error
|
||||
}
|
||||
|
||||
type (
|
||||
CompanySecrets any
|
||||
|
||||
client struct {
|
||||
cache cache2.Client
|
||||
db database.Client
|
||||
logger *slog.Logger
|
||||
secrets map[string]CompanySecrets
|
||||
}
|
||||
|
||||
Config struct {
|
||||
Cache cache2.Client
|
||||
Db database.Client
|
||||
Logger *slog.Logger
|
||||
Secrets map[string]CompanySecrets
|
||||
}
|
||||
)
|
||||
|
||||
func New(c Config) (Client, error) {
|
||||
return &client{
|
||||
cache: c.Cache,
|
||||
db: c.Db,
|
||||
logger: c.Logger,
|
||||
secrets: c.Secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: need to think about possible errors
|
||||
// e.g. cache is down, connection lost etc.
|
||||
|
||||
func (c *client) HandleVacancyResponse(request HandleVacancy) error {
|
||||
const handlerName = "integration.HandleVacancyResponse"
|
||||
|
||||
companyId, vacancyExtraFields, err := c.getVacancyIntegrationInfo(context.Background(), request.VacancyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vacancyExtraFields == nil {
|
||||
c.logger.Debug("vacancy extra fields are empty",
|
||||
slog.String("vacancy_id", request.VacancyId),
|
||||
slog.String("handler", handlerName),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
companyMetadata, companyExtraFieldsTemplate, err := c.getCompanyIntegrationInfo(context.Background(), companyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if companyMetadata == nil {
|
||||
c.logger.Debug("company metadata is empty",
|
||||
slog.String("company_id", companyId),
|
||||
slog.String("handler", handlerName),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if companyExtraFieldsTemplate == nil {
|
||||
c.logger.Debug("company extra fields template are empty",
|
||||
slog.String("company_id", companyId),
|
||||
slog.String("handler", handlerName),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
companyName, ok := (*companyMetadata)[companyNameMetadataKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("company name not found in metadata")
|
||||
}
|
||||
|
||||
switch companyName {
|
||||
case VkusvillCompanyName:
|
||||
return c.handleVkusvillIntegration(
|
||||
context.Background(),
|
||||
request,
|
||||
vacancyExtraFields,
|
||||
companyMetadata,
|
||||
companyExtraFieldsTemplate,
|
||||
)
|
||||
|
||||
default:
|
||||
return ErrUnknownIntegration
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocognit // not that hard
|
||||
func (c *client) getVacancyIntegrationInfo(
|
||||
ctx context.Context,
|
||||
vacancyId string,
|
||||
) (string, *VacancyExtraFields, error) {
|
||||
const handlerName = "integration.getVacancyIntegrationInfo"
|
||||
|
||||
var (
|
||||
companyId string
|
||||
vacancyExtraFields VacancyExtraFields
|
||||
gotCached = true
|
||||
)
|
||||
|
||||
// get company id and vacancy filled template
|
||||
cachedIntegrationInfo, err := c.cache.Get(context.Background(), vacancyId, cache.IntegrationVacancyValueType)
|
||||
if err != nil && !errors.Is(err, cache.ErrKeyNotFound) {
|
||||
if err := tryRepeat(context.Background(), func() error {
|
||||
cachedIntegrationInfo, err = c.cache.Get(context.Background(), vacancyId, cache.IntegrationCompanyValueType)
|
||||
return err
|
||||
}, defaultRetryAmount); err != nil {
|
||||
// TODO: maybe we should process further and try DB instead
|
||||
// Need to handle the error deeper
|
||||
// e.g. if cache is down we can try to get info from DB
|
||||
// on the other hand, if there is any other fatal error, we should return it
|
||||
return "", nil, fmt.Errorf("error getting vacancy integration info from cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, cache.ErrKeyNotFound) {
|
||||
// try get data from DB
|
||||
dbCompanyId, dbFields, err := c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId)
|
||||
if err != nil {
|
||||
if errors.Is(err, dberrors.ErrNotFound) {
|
||||
return "", nil, ErrVacancyNotFound
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := tryRepeat(ctx, func() error {
|
||||
dbCompanyId, dbFields, err = c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId)
|
||||
return err
|
||||
}, 0); err != nil {
|
||||
return "", nil, fmt.Errorf("error getting vacancy integration info from DB: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
companyId = dbCompanyId
|
||||
vacancyExtraFields = *new(VacancyExtraFields).From(dbFields)
|
||||
gotCached = false
|
||||
|
||||
// store obtained info in cache
|
||||
fullMetadata := VacancyIntegrationInfo{
|
||||
CompanyId: companyId,
|
||||
ExtraFields: vacancyExtraFields,
|
||||
}
|
||||
|
||||
jsonMetadata, err := json.Marshal(fullMetadata)
|
||||
if err != nil {
|
||||
c.logger.Error("error marshalling vacancy integration info",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("vacancy_id", vacancyId),
|
||||
slog.String("handler", handlerName))
|
||||
} else {
|
||||
if err := tryRepeat(context.Background(), func() error {
|
||||
return c.cache.Set(context.Background(), vacancyId, cache.IntegrationVacancyValueType, string(jsonMetadata), 0)
|
||||
}, defaultRetryAmount); err != nil {
|
||||
c.logger.Error("error saving vacancy integration info to cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("vacancy_id", vacancyId),
|
||||
slog.String("handler", handlerName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gotCached {
|
||||
var fullMetadata VacancyIntegrationInfo
|
||||
if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil {
|
||||
return "", nil, fmt.Errorf("error unmarshalling cached metadata: %w", err)
|
||||
}
|
||||
|
||||
companyId = fullMetadata.CompanyId
|
||||
vacancyExtraFields = fullMetadata.ExtraFields
|
||||
}
|
||||
|
||||
return companyId, &vacancyExtraFields, nil
|
||||
}
|
||||
|
||||
//nolint:gocognit // not that hard
|
||||
func (c *client) getCompanyIntegrationInfo(
|
||||
ctx context.Context,
|
||||
companyId string,
|
||||
) (*CompanyMetadata, *CompanyExtraFieldsTemplate, error) {
|
||||
const handlerName = "integration.getCompanyIntegrationInfo"
|
||||
|
||||
var (
|
||||
companyMetadata CompanyMetadata
|
||||
companyFieldsTemplate CompanyExtraFieldsTemplate
|
||||
gotCached = true
|
||||
)
|
||||
|
||||
// get company metadata and template
|
||||
cachedIntegrationInfo, err := c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType)
|
||||
if err != nil && !errors.Is(err, cache.ErrKeyNotFound) {
|
||||
if err := tryRepeat(context.Background(), func() error {
|
||||
cachedIntegrationInfo, err = c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType)
|
||||
return err
|
||||
}, defaultRetryAmount); err != nil {
|
||||
// TODO: maybe we should process further and try DB instead
|
||||
// Need to handle the error deeper
|
||||
// e.g. if cache is down we can try to get info from DB
|
||||
// on the other hand, if there is any other fatal error, we should return it
|
||||
return nil, nil, fmt.Errorf("error getting company metadata from cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, cache.ErrKeyNotFound) {
|
||||
// try get data from DB
|
||||
dbMetadata, dbFieldsTemplate, err := c.db.GetCompanyMetadataById(context.Background(), companyId)
|
||||
if err != nil {
|
||||
if errors.Is(err, dberrors.ErrNotFound) {
|
||||
return nil, nil, ErrCompanyNotFound
|
||||
}
|
||||
|
||||
// TODO: think about other retry policies
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := tryRepeat(ctx, func() error {
|
||||
dbMetadata, dbFieldsTemplate, err = c.db.GetCompanyMetadataById(context.Background(), companyId)
|
||||
return err
|
||||
}, 0); err != nil {
|
||||
return nil, nil, fmt.Errorf("error getting company integration info from DB: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
companyMetadata = *new(CompanyMetadata).From(dbMetadata)
|
||||
companyFieldsTemplate = *new(CompanyExtraFieldsTemplate).From(dbFieldsTemplate)
|
||||
gotCached = false
|
||||
|
||||
// store obtained info in cache
|
||||
fullMetadata := CompanyIntegrationInfo{
|
||||
Metadata: companyMetadata,
|
||||
ExtraFieldsTemplate: companyFieldsTemplate,
|
||||
}
|
||||
|
||||
jsonMetadata, err := json.Marshal(fullMetadata)
|
||||
if err != nil {
|
||||
c.logger.Error("error marshalling company integration info",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyId),
|
||||
slog.String("handler", handlerName))
|
||||
} else {
|
||||
if err := tryRepeat(context.Background(), func() error {
|
||||
return c.cache.Set(context.Background(), companyId, cache.IntegrationCompanyValueType, string(jsonMetadata), 0)
|
||||
}, defaultRetryAmount); err != nil {
|
||||
c.logger.Error("error saving company integration info to cache",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("company_id", companyId),
|
||||
slog.String("handler", handlerName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gotCached {
|
||||
var fullMetadata CompanyIntegrationInfo
|
||||
if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil {
|
||||
return nil, nil, fmt.Errorf("error unmarshalling cached metadata: %w", err)
|
||||
}
|
||||
|
||||
companyMetadata = fullMetadata.Metadata
|
||||
companyFieldsTemplate = fullMetadata.ExtraFieldsTemplate
|
||||
}
|
||||
|
||||
return &companyMetadata, &companyFieldsTemplate, nil
|
||||
}
|
||||
|
||||
// tryRepeat tries to execute function N times.
|
||||
// If number of attempts is 0, the function will be executed until context is canceled
|
||||
func tryRepeat(ctx context.Context, fn func() error, attempts uint) error {
|
||||
ch := make(chan error, 1)
|
||||
|
||||
for iter := uint(0); iter < attempts || attempts == 0; iter++ {
|
||||
go func() {
|
||||
ch <- fn()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if attempts != 0 && iter == attempts-1 {
|
||||
return err
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user