ActiveRecord (Rails 3.0.1): API Can't Handle Too Many Requests? - ruby-on-rails-3

I have an API that services a web-based plugin for processing email. The API is responsible for two things:
Creating SessionIDs so the plugin can setup a dynamic link; and
Once an email is sent, for receiving that SessionID, the email recipients and subject line, to store the information into a new session.
Imagine the scenario where the plugin sends a request to the API:
PUT http://server.com/api/email/update/<SessionID> -d "to=<address1,address2>&subject=<subject>"
In testing this works fine: the data is saved normally. However, the plugin can't help but send that request several times a second, bombarding my server with identical requests. The result is that I get my EmailSession object saving multiple copies of the recipients.
In terms of my database schema, I have an EmailSession model, which has_many EmailRecipients.
Here's the relevant part of the update method in my API's controller:
#email_session = EmailSession.find_or_create_by_session_id(:session_id => params[:id], :user_id => #user.id)
if opts[:params][:cm_to].blank? == false
self.email_recipients.destroy_all
unless opts[:params][:cm_to].blank?
opts[:params][:cm_to].strip.split(",").each do |t|
self.email_recipients << EmailRecipient.create(:recipient_email => t)
end
end
end
Admittedly, the "find_or_create" dynamic method is new to me, and I wonder if there's something about that screwing up the works.
The symptoms I'm seeing include:
ActiveRecord errors complaining about attempts to save a non-unique key into the database (I have an index on the SessionId)
Duplicate recipients ending up in the EmailRecipients collection
In the case of multiple users employing the plugin, I get recipients from other emails ending up in the wrong email session collections.
I've attempted to employ delayed_job to attempt to serialize these requests somehow. I haven't had much luck with it thanks to various bugs in the current release. But I'm wondering if there's a more fundamental problem with my approach to this solution? Any help would be appreciated.

I'm still not sure I understand what you're doing, but here's my advice.
First off I don't think you are using find_or_create_by properly. This method has slightly confusing semantics (which is why 3.2 introduces some clearer alternatives) but as it stands it isn't using the user_id to find the record (although it is setting user_id if a record is created). I don't think this is what you wanted. Instead use find_or_create_by_session_id_and_user_id
This can still raise a duplicate key error since in between find_or_create checking and it creating the record there is time for someone else to create the record. If you weren't doing anything other than creating email session rows the  rescuing this duplicate key error and then retrying should take of that: on the retry you'll find the row that blocked your insert.
However when you then go on to add recipients you still have a potential issue because 2 things could be trying to remove recipients and add them to the same email session at the same time. This might be a good usecase for pessimistic locking. 
begin
EmailSession.transaction do
session = EmailSession.lock(true).find_or_create_by_bla_bla(...)
# use the session object here, add recipients etc.
end
rescue ActiveRecord::StatementInvalid => e
end
What is happening here is that when the email session is retrieved from the db, the row is locked (even if it doesn't exist yet - effectively you can lock the gap where the record would go). This means that anyone else wanting to add recipients or do any other manipulation has to wait for the lock to be released. Locks last as long as the transaction in which they occur lasts  so all your work should happen in here (even if in the second part you are not actually changing the email session object any more).
You may end up with deadlocks - I don't know what else is going on in your app but you should be prepared for them if you are using transactions. That's what the rescue block is for: if the error message looks like a deadlock then you should probably retry some limited number of times.
Locks are (at least on MySQL) row level locks: as long as you have an index on session_id,user_id then just because one of your instance has one email session object locked doesn't stop another instance from using another one.

Related

GoogleCalendarAPI to insert events giving Rails--[API Error]: --{:error=>["Excon::Error::Forbidden", "Expected([200]) <=> Actual(403 Forbidden)\n"]}

I have been using Google calendar API to insert events in user's calendar. I have written a code which takes csv(with approx 1000 rows) as an input with minimum required parameters to create events such as event_title, event_description, event_date (since its a full day event) and recipient email id..The code runs in background using sidekiq.
Out of 1000 rows , only some events are inserted in user's calendar. Most of them are giving Rails -- [API Error]: -- {:error=>["Excon::Error::Forbidden", "Expected([200]) <=> Actual(403 Forbidden)\n"]} error.
I have been using excon client (gem) to hit the urls to refresh token and inserting an event.
Token is refreshed
Loop (
Hitting API to insert events
)
rescue error
end
I couldnt figure out what's going wrong ?..Any help is appreciated. While registering my app in Google developer console , I have given the full scope of calendar api auth/calendar.
I tried setting Excon.defaults[:ssl_verify_peer] = false.
Implementing such api first time.
The 403 would indicate that the permissions are not valid in some way. My guesses would be that either the token is expiring, you are hitting some kind of rate limit, or something is different about the parameters for the failing request such that the particular event is not allowed.
Do you think any of those is more likely than the others? If it is the token expiring, you could try to catch the 403 error, refresh the token, and retry. If it's one of the others it might take a bit more digging to see why the ones that are failing are different from the ones that succeed.

Trying to use adminp.DeleteReplicas followed by adminp.ApproveReplicaDeletion gives error "Invalid Approval Request note"

I am trying delete a database and any associated replicas using LotusScript adminp calls. This is basically the code:
Dim session As New NotesSession
Dim adminp As NotesAdministrationProcess
Set adminp = session.CreateAdministrationProcess("Software_Server")
noteid$ = adminp.DeleteReplicas("Software_Server", "Guys1")
If noteid$ <> "" Then
Call adminp.ApproveReplicaDeletion(noteid$) 'This is where the error is thrown
End If
The first adminp call is successful and returns a noteid, and if I look in the admin requests database can see the document. The next call to ApproveReplicateDeletion results in the error "Invalid Approval Request note"
The documentation doesn't contain any examples for the adminp approve methods. I have a feeling that maybe the second request cannot be called until much later when adminp has processed the first request?
Also related question, do I only have to make this request on a single server and it will remove replicas on all other servers, or do I need to make this request for each server?
So this is a bit more complicated than the help gives any indication of, and might explain why I couldn't find any examples on the internet on how to do it. So the Workflow for using AdminP is as Follows:
Create the request to get the noteID for the initial noteid of the DeleteReplicas request. The returned noteID is not the one that is used to approve the delete replica request. noteid$ = adminp.DeleteReplicas("Software_Server", "Guys1")
This creates a document in the admin.nsf db for the initial request, but the approval docs don't yet exist, for the server to create those, the adminp process must run.
So in the code send a Console command to the Server "tell adminp process now"
Sleep the agent for a few seconds to give adminp time to process the request (this will also fire off any other waiting adminp requests unfortunately)
Now new documents will have been created in the admin database, that are awaiting approval by an admin. These documents contain the noteids that should be sent the approvereplicadeletion
To get them, first lookup the notes document by the noteID in the admin db obtained in step 1
Using that noteid for the document, get the field value ProxyOriginatingRequestUNID from the document.
using this UNID value, perform a getalldocumentsbykey on the view ($AllRequestsbyOriginatingUNID)
if the returned document has a ProxyAction field with a value of "82", this is an approval request document. This documents noteid can be passed to approveReplicaDeletion to have adminp remove the database next time it processes requests.
You can either send a console command to process adminp again, or just wait for the database deletions etc. to happen next time around.

How to internationalization errors returned by API?

I'm wondering how to manage error in an API with multiple language.
I know there are multiple way to do that. I found 3.
Send a statusCode to front (with optionnal some data to manage dynamics values like : "you can manage only X users")
Send errors for all language ex :
{
en:"error is....",
fr:"l'erreur est...",
...
}
send just one error in the correct language
{
en:"error is...."
}
All of this have pro and cons...
the first :
+ API just manage statusCode and front(s) can manage error how it want
- API must manage dynamics error and send extra data
- Front must reformat the error (error + extra data)
others ?
the second :
+ error message can just be displayed on front without update it (but need to get the good one for the language client)
- back must manage error message for all language (if there are 10-50 or more language it's a little bit annoying)
- back end must take care of displaying message interstanding for a user (I think it's not is role)
others ?
the third :
+ error message can just be displayed on front without update it
- back end must take care of displaying message interstanding for a user (I think it's not is role)
- front must send the user's language at the session's begining and update this information if user change it
others ?
I think the third is the better but I don't know.
someone could say me what is the best solution and why ?
Thanks in advance :)

ActiveRecord.establish_connection concurrency across requests

Using Rails, is this approach vulnerable to causing separate, concurrent requests to use the wrong connection/database?
 def find_user
#user= User.find_by_email(email)
unless #user
#search non_pro database
User.establish_connection("#{Rails.env}_non_pro".to_sym)
#user= User.find_by_email(email)
end
ensure
User.establish_connection("#{Rails.env}".to_sym)
end
What I want to know is if a first request causes the connection to be established with 'non_pro', will a second separate request run the risk of incorrectly connecting to 'non_pro' if it runs while the first is connected to 'non_pro'
I did a small test and found out that if you do User.establish_connection() this will change the connection globally for all concurrent requests, so they might fail by using the wrong database.
What you can do is making a subclass of User
class NonProUser < User
self.table_name = "users"
establish_connection("#{Rails.env}_non_pro".to_sym)
end
Then you can use NonProUser.find_by_email(email) to find your user on the other database.

How to cancel a deferred NServiceBus Message?

Say I use the deferred messaging feature to send a message at some later future point in time, but then later I might want to cancel that message.
Question 1 - When making the original bus.Defer(...) call, how do I get a unique identifier back for that message? I would expect there to be a message id or a timeout id of some sort.
Question 2 - Short of calling the RavenDB database directly, is there a way to query the bus to get back all pending deferred messages?
Question 3 - Is there some way to cancel a deferred message? I would expect something like bus.CancelDeferred(messageid)
Is any of this available, or are there any other mechanisms I can use to achieve similar results?
I've had the need to abandon deferred messages and outstanding replies a few times, and I did it by "incrementing the correlation ID" on my saga. You don't mention sagas though, so I'm not sure if this solution will be usable to you. I do think, however, that it goes under "any other mechanisms" that you ask for :)
Check out this example - here I have the state of my saga which, among other things, contains a custom CorrelationId:
public class MySagaData : ISagaData
{
// ... the usual stuff here
public string CorrelationId { get; set; }
}
and then, each time I defer a message or request something, I correlate the deferred message and/or reply with the current value of the correlation ID:
bus.Defer(time, new Something { CorrelationId = Data.CorrelationId });
bus.Send(new SomeRequest { CorrelationId = Data.CorrelationId });
thus conceptually correlating the deferred message and the reply with the current state of the saga.
And then, in cases where I want to abandon all outstanding messages, I simply re-set the saga's correlation ID to a new value - I usually set the value to something like somethingWithBusinessMeaning/timestamp.
This way, abandoned messages will not correlate with any saga instance, effectively being ignored.
Does it make sense?
1) There is currently no way to do this. You could add your own header with a app specific id if you need to keep track of them
2) No, you have to query the storage as you mention. That said what would be the use case for this?
3) No, and this is by design. Given that you can't assume when a message will arrive you can't rely on timeouts being canceled since a defered message might be stuck in a queue and processed right after you cancel. The "cancel" message might also get lost.
In short: your code needs to be prepared to discard "invalid" messages no matter what.