-
Notifications
You must be signed in to change notification settings - Fork 2
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
Make sure the HTTP call is never made inside a database transaction. #1099
base: master
Are you sure you want to change the base?
Changes from 10 commits
1e4c869
acd5bf0
2b87da4
faf2c9f
3e53ee0
4ecc6bf
d4ecc20
469b28f
354d2e5
dd140bf
f74233d
46b8a27
3c34f0e
2626486
460f923
bd079a0
27d5a62
0415b3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ import ( | |
"context" | ||
"time" | ||
|
||
"github.com/France-ioi/AlgoreaBackend/app/utils" | ||
|
||
"github.com/France-ioi/AlgoreaBackend/app/rand" | ||
) | ||
|
||
|
@@ -176,10 +178,11 @@ func (s *DataStore) NewID() int64 { | |
} | ||
|
||
type awaitingTriggers struct { | ||
ItemAncestors bool | ||
GroupAncestors bool | ||
Permissions bool | ||
Results bool | ||
ItemAncestors bool | ||
GroupAncestors bool | ||
Permissions bool | ||
Results bool | ||
SchedulePropagationTypes []string | ||
} | ||
type dbContextKey string | ||
|
||
|
@@ -202,6 +205,12 @@ func (s *DataStore) InTransaction(txFunc func(*DataStore) error) error { | |
|
||
triggersToRun := s.ctx.Value(triggersContextKey).(*awaitingTriggers) | ||
|
||
if len(triggersToRun.SchedulePropagationTypes) > 0 { | ||
types := triggersToRun.SchedulePropagationTypes | ||
triggersToRun.SchedulePropagationTypes = []string{} | ||
|
||
StartAsyncPropagation(s, s.Context().Value("propagation_endpoint").(string), types) | ||
} | ||
if triggersToRun.GroupAncestors { | ||
triggersToRun.GroupAncestors = false | ||
s.createNewAncestors("groups", "group") | ||
|
@@ -222,6 +231,14 @@ func (s *DataStore) InTransaction(txFunc func(*DataStore) error) error { | |
return err | ||
} | ||
|
||
// SchedulePropagation schedules a run of the propagation for the given types after the transaction commit. | ||
func (s *DataStore) SchedulePropagation(types []string) { | ||
s.mustBeInTransaction() | ||
|
||
triggersToRun := s.DB.ctx.Value(triggersContextKey).(*awaitingTriggers) | ||
triggersToRun.SchedulePropagationTypes = utils.UniqueStrings(append(triggersToRun.SchedulePropagationTypes, types...)) | ||
} | ||
|
||
// ScheduleResultsPropagation schedules a run of ResultStore::propagate() after the transaction commit. | ||
func (s *DataStore) ScheduleResultsPropagation() { | ||
s.mustBeInTransaction() | ||
|
@@ -262,10 +279,6 @@ func (s *DataStore) WithForeignKeyChecksDisabled(blockFunc func(*DataStore) erro | |
}) | ||
} | ||
|
||
func (s *DataStore) IsInTransaction() bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was a nice useful method. |
||
return s.DB.isInTransaction() | ||
} | ||
|
||
// WithNamedLock wraps the given function in GET_LOCK/RELEASE_LOCK. | ||
func (s *DataStore) WithNamedLock(lockName string, timeout time.Duration, txFunc func(*DataStore) error) error { | ||
return s.withNamedLock(lockName, timeout, func(db *DB) error { | ||
|
@@ -327,3 +340,8 @@ func (s *DataStore) InsertOrUpdateMap(dataMap map[string]interface{}, updateColu | |
func (s *DataStore) InsertOrUpdateMaps(dataMap []map[string]interface{}, updateColumns []string) error { | ||
return s.DB.insertOrUpdateMaps(s.tableName, dataMap, updateColumns) | ||
} | ||
|
||
// MustNotBeInTransaction panics if the store is in a transaction. | ||
func (s *DataStore) MustNotBeInTransaction() { | ||
s.DB.mustNotBeInTransaction() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,26 @@ | ||
package service | ||
package database | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/France-ioi/AlgoreaBackend/app/database" | ||
"github.com/France-ioi/AlgoreaBackend/app/logging" | ||
) | ||
|
||
const PropagationEndpointTimeout = 3 * time.Second | ||
const endpointTimeout = 3 * time.Second | ||
|
||
// SchedulePropagation schedules asynchronous propagation of the given types. | ||
// StartAsyncPropagation schedules asynchronous propagation of the given types. | ||
// If endpoint is an empty string, it will be done synchronously. | ||
func SchedulePropagation(store *database.DataStore, endpoint string, types []string) { | ||
func StartAsyncPropagation(store *DataStore, endpoint string, types []string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would move this method into app/service. It can be called right from the endpoint handler in case when the async propagation is chosen. The database package is for database-related things only. |
||
// Must not be called in a transaction because it calls an endpoint, which can take a long time. | ||
store.MustNotBeInTransaction() | ||
|
||
endpointFailed := false | ||
if endpoint != "" { | ||
// Async. | ||
client := http.Client{ | ||
Timeout: PropagationEndpointTimeout, | ||
Timeout: endpointTimeout, | ||
} | ||
|
||
callTime := time.Now() | ||
|
@@ -43,20 +45,13 @@ func SchedulePropagation(store *database.DataStore, endpoint string, types []str | |
} | ||
|
||
if endpoint == "" || endpointFailed { | ||
// Sync. | ||
if store.IsInTransaction() { | ||
err := store.InTransaction(func(store *DataStore) error { | ||
store.ScheduleItemsAncestorsPropagation() | ||
store.SchedulePermissionsPropagation() | ||
store.ScheduleResultsPropagation() | ||
} else { | ||
err := store.InTransaction(func(store *database.DataStore) error { | ||
store.ScheduleItemsAncestorsPropagation() | ||
store.SchedulePermissionsPropagation() | ||
store.ScheduleResultsPropagation() | ||
|
||
return nil | ||
}) | ||
MustNotBeError(err) | ||
} | ||
|
||
return nil | ||
}) | ||
mustNotBeError(err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Public methods cannot return errors via panic, panic/recover pattern should be used only within a package (see https://go.dev/doc/effective_go#recover). Also, it is an "architecture decision" made in March 2019, which we have always been following since then. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the order of types doesn't matter, it would be much cleaner to store map[string]struct{} in triggersToRun.SchedulePropagationTypes instead of creating utils.UniqueStrings() and tests for it. Also, the map works faster. But a struct containing all the possible values as boolean fields (like 'awaitingTriggers') would be even better than the map: it would store only several booleans while the map stores strings. Also, the struct provides immediate access to its fields, while the map uses hashing. Also, with the struct, it is not possible to mistype a value which is an often issue with maps/slices.
If the order of types matters, UniqueString() breaks it anyway since maps don't preserve the order of keys.