-
Notifications
You must be signed in to change notification settings - Fork 29
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
🚀 Imports 2.0 #358
🚀 Imports 2.0 #358
Conversation
Codecov Report
@@ Coverage Diff @@
## master #358 +/- ##
==========================================
+ Coverage 53.00% 54.01% +1.01%
==========================================
Files 109 111 +2
Lines 7856 8010 +154
==========================================
+ Hits 4164 4327 +163
+ Misses 3007 2959 -48
- Partials 685 724 +39
Continue to review full report at Codecov.
|
6b59a04
to
c159526
Compare
models/contacts.go
Outdated
// * If URNs exists and belongs to a single contact it returns that contact (other URNs are not assigned to the contact). | ||
// * If URNs exists and belongs to multiple contacts it will return an error. | ||
// | ||
func GetOrCreateContact(ctx context.Context, db *sqlx.DB, oa *OrgAssets, urnz []urns.URN) (*Contact, *flows.Contact, error) { |
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.
Contract is a bit different than what we've had in RP - we don't promise that the returned contact owns all of the passed in URNs - instead the import code assigns any missing URNs using modifiers. I like this because:
- Unless we added locking - that stuff isn't going to be atomic anyway
- We don't duplicate the logic around creating new URNs vs attaching orphaned URNs in that modifier
- Using modifiers means there's an event to say that a URN was added to a contact... and one day we'll show all events for all contacts... one day
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.
see nyaruka#360
d524ddd
to
daed23a
Compare
// CreateContact creates a new contact for the passed in org with the passed in URNs | ||
func CreateContact(ctx context.Context, db *sqlx.DB, oa *OrgAssets, userID UserID, name string, language envs.Language, urnz []urns.URN) (*Contact, *flows.Contact, error) { | ||
// find current owners of these URNs | ||
owners, err := contactIDsFromURNs(ctx, db, oa.OrgID(), urnz) |
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.
same check to see if URNs are taken added here to avoid even more tx rollbacks
return contactID, true, nil | ||
} | ||
|
||
if dbutil.IsUniqueViolation(err) { |
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.
thinking of ways to test this... could have another interface like Queryer
that supports transactions (QueryerWithTx
).. use that everywhere instead of *sqlx.DB
, and make an implementation for testing that we can configure to blow up when needed
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.
Don't know if this is the best, or even a good way, but in Courier we do in goroutines by having a flag we can set on the test to slow things down:
https://github.com/nyaruka/courier/blob/master/backends/rapidpro/backend_test.go#L254
https://github.com/nyaruka/courier/blob/master/backends/rapidpro/contact.go#L180
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.
Interesting - I like the idea of being able to test an actual race - tho I think I'd still want to keep that out of the production code. I.e. could use the db interface as per nyaruka#360 but make one of those calls take an extra second.
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.
models/contacts.go
Outdated
if err != nil { | ||
return nil, errors.Wrapf(err, "error selecting contact ids by uuid") | ||
return nil, err |
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.
Remember go annoyingly treats no rows as an error.
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.
So the code that it replaces wasn't checking for sql.NoRows
because it's only only sued in places where you're expecting get back ids for things that should exist.. so I went back and forth on whether this being a generic utility method should allow that.. and couldn't make up my mind.. but this morning I'm leaning toward it allowing no rows, and the calling code should check that it got as many ids as it expected
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.
Nice
models/contacts.go
Outdated
} | ||
defer rows.Close() | ||
|
||
var id ContactID | ||
for rows.Next() { | ||
err := rows.Scan(&id) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error scanning contact id") | ||
return nil, err |
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.
What's your rule for wrapping or not wrapping? I kinda just defer to always wrapping because hey, more info when looking at a trace never seems like a bad thing but totally admit maybe that's overkill. Just wondering what rule you use.
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.
I've generally leaned toward always wrap - not even sure why I didn't here - fine with more wrapping
return contactID, true, nil | ||
} | ||
|
||
if dbutil.IsUniqueViolation(err) { |
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.
Don't know if this is the best, or even a good way, but in Courier we do in goroutines by having a flag we can set on the test to slow things down:
https://github.com/nyaruka/courier/blob/master/backends/rapidpro/backend_test.go#L254
https://github.com/nyaruka/courier/blob/master/backends/rapidpro/contact.go#L180
// import status constants | ||
const ( | ||
ContactImportStatusPending ContactImportStatus = "P" | ||
ContactImportStatusProcessing ContactImportStatus = "O" |
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.
Would Running
be better maybe?
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.
Maybe but these are the same codes that we use in the 3 export models
func (b *ContactImportBatch) Import(ctx context.Context, db *sqlx.DB, orgID OrgID) error { | ||
// if any error occurs this batch should be marked as failed | ||
if err := b.tryImport(ctx, db, orgID); err != nil { | ||
b.markFailed(ctx, db) |
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.
Any benefit to logging the error in the task itself too?
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.
I think this is an error for us (it's a a bunch of wrapped err's) rather than the user facing errors what we collect in batch.errors
and relate to problems in the user's data, so it's fine if it just goes to sentry.
} | ||
|
||
// and apply in bulk | ||
_, err = ApplyModifiers(ctx, db, nil, oa, modifiersByContact) |
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.
May not be worth it but one thing we do in batch starts is if we fail we retry in individual transactions for each contact.. this helps with deadlocks.
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.
So that would be something like reworking HandleAndCommitEvents
to catch any error when committing the transaction that wraps ApplyEventPreCommitHooks
.. and if it fails, try each scene one by one? That sounds pretty straightforward.. tho inclined to try without first.
models/imports.go
Outdated
b.FinishedOn = &now | ||
_, err = db.ExecContext(ctx, | ||
`UPDATE contacts_contactimportbatch SET status = $2, num_created = $3, num_updated = $4, num_errored = $5, errors = $6, finished_on = $7 WHERE id = $1`, | ||
b.ID, b.Status, b.NumCreated, b.NumUpdated, b.NumErrored, b.Errors, b.FinishedOn, |
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.
Since you have a batch at this point maybe cleaner / less error prone to use NamedQuery?
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.
yeah will update
add is_resend to Msg payload to allow for resending messages manually
No description provided.