Reputation: 41
I have an application that offers some offers for some stuff. The idea is the offer is added by me on another application I created and then be shown on users devices all at the same time.
I need only one user to be able to take the offer so when the first user clicks I do call removeValue() on the offer ref. The offer is correctly deleted from the database and from the other users recyclerview.
The problem is when 2 clicks happens in the same time the offer is deleted but onChildRemoved() doesn't have the time to be called so both users now have the same offer!
Is there any other idea how to make this operation more precise and time aware?
UPDATE as suggested from svi.data i tried this piece of code on user click but still the same problem occur.
offerUnAnsweredRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
boolean stillThere = false;
for (DataSnapshot offerSnap : dataSnapshot.getChildren()) {
if (offerSnap.getKey().equals(requestedOffer.getCurrentNodeKey())) {
stillThere = true;
}
}
if (stillThere) {
Timber.d("We have it " + requestedOffer.getEmployeeKey());
Toast.makeText(getContext(), "Welcome Dear ", Toast.LENGTH_SHORT).show();
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).removeValue();
} else {
Toast.makeText(getContext(), "Go Away Bear", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
UPDATE 2
Actually the solution is built on top of svi.data answer with some modifications so i wanted to share the working code to help if any one ever come cross similar situation
offerUnAnsweredRef.child(requestedOffer.getCurrentNodeKey()).runTransaction(new Transaction.Handler() {
@NonNull
@Override
public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
RequestedOffer o = mutableData.getValue(RequestedOffer.class);
if (o == null) {
return Transaction.abort();
}
if (o.getEmployeeKey() == null) {
o.setEmployeeKey(employee.getUid());
mutableData.setValue(o);
return Transaction.success(mutableData);
} else {
return Transaction.success(mutableData);
}
}
@Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// check if the transaction completed successfully
// or if it failed
RequestedOffer o = dataSnapshot.getValue(RequestedOffer.class);
if (o.getEmployeeKey() == employee.getUid()) {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "Hello", Toast.LENGTH_SHORT).show());
DatabaseReference databaseReference = FirebaseFactory.getDatabase()
} else {
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "NO", Toast.LENGTH_SHORT).show());
}
}
because as documentation says
public abstract Transaction.Result doTransaction (MutableData currentData)
This method will be called, possibly multiple times, with the current data at this location. It is responsible for inspecting that data and returning a Transaction.Result specifying either the desired new data at the location or that the transaction should be aborted.
So i added the code to check in onComplete to ensure that it called only once.
Upvotes: 1
Views: 248
Reputation: 4035
From what I understand:
1) You have a specific app for adding the offers (by you).
2) You have another app for reading the offers (by users).
3) If this is the case then both apps use the same project.
4) When a user clicks an offer, he/she will get the offer, then you will delete the offer from the database.
5) Now when 2 users click the same offer there is no time for the offer to be removed from the other user's list, so they end up with the same offer.
Now it seems that you don't want users to get same offers, and the problem really is a timing issue.
Possible solution:
1) When ever a user clicks an offer, you run a ValueEventListener() to the offers
node in the database and check if the offer exist.
2) If the offer exists give him/her the offer and delete it.
3) Now when 2 users click the same offer, the ValueEventListener that I talked about will provide you with some time before reacting.
4) So users shouldn't end up with same offers.
Hope it solves your problem.
UPDATE:
As this is is a race condition between users, its time to talk about transactions. Firebase provides a nice way to directly read and write concurrent updates to the same node (which is your case).
I want your database to be like this:
Offers
|
|------offer_id_1
|
|-----taken:false
|-----......
|-----......
|
|-------offer_id_2
|
|------taken:false
|------......
|------......
Let me explain the above structure, each offer you post from the other application will have a flag by default called taken
and it should have by default a value of false
.
Now as you see above offer_id_1
and offer_id_2
are the push id or the random id given for the offer (when a user click on an offer you must get a reference of this key....I assume you know how to do this).
Before we start ofcourse you should have a model class for your posts we will call it Offer
its just a class:
public class Offer{
public boolean taken;
......
......
}
The below function is what you will call after someone clicked an offer (we will use a transaction):
public void RunTransactionFor(String offer_id){
//first refer to your offers
DatabaseReference offers_ref = FirebaseDatabase.getInstance().getReference().child("offers").child(offer_id);
//run a transaction (a transaction is fast it reads and writes directly)
offer_ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
//this is a ref to the offer class
Offer offer = mutableData.getValue(Offer.class);
if (offer == null) {
return Transaction.success(mutableData);
}
if(offer.taken == false){
//take the offer
offer.taken = true;
//show a message
Toast.makeText(context, "you took the offer",...).show();
//now you can remove the offer
offers_ref.setValue(null);//or delete it your way
}else{
//too late the offer is taken
Toast.makeText(context, "too late the offer is gone",...).show();
//do nothing
}
// Set value and report transaction success
mutableData.setValue(offer);
return Transaction.success(mutableData);
}
@Override
public void onComplete(DatabaseError databaseError, boolean b,
DataSnapshot dataSnapshot) {
// Transaction completed
}
});
}
Now when a user clicks an offer in the list store the id of the offer and pass it to the above function like this
//after user clicks
String offer_id = .......
//run transaction
RunTransactionFor(offer_id);
Note: Transactions only work online they can't work offline.
Upvotes: 2