0

I have some design/implementation issue that I just can't wrap my head around it. I am currently working on a text-based game with multiple players. I kind of understand how it works for Player-to-Server, I meant that Server sees every individual Player as the same.

I'm using spring-boot 2, spring-web, thymeleaf, hibernate.

I implemented a custom UserDetails that returns after the user login.

@Entity
@Table(name = "USER")
public class User implements Serializable {

    @Id
    private long userId;

    @Column(unique = true, nullable = false)
    private String userName;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "playerStatsId")
    private PlayerStats stats;
}

public class CurrentUserDetailsService implements UserDetailsService {

    @Override
    public CurrentUser loadUserByUsername(String userName) {

        User user = this.accountRepository.findByUserName(userName)
                .orElseThrow(() -> 
                    new UsernameNotFoundException("User details not found with the provided username: " + userName));

        return new CurrentUser(user);
    }
}

public class CurrentUser implements UserDetails {

    private static final long serialVersionUID = 1L;
    private User user = new User();

    public CurrentUser(User user) {
        this.user = user;
    }

    public PlayerStats getPlayerStats() {
        return this.user.getStats();
    }

    // removed the rest for brevity 
}

Hence, in my controller, I can do this to get the CurrentUser. *Note each User is also a player.

@GetMapping("/attackpage")
public String viewAttackPage(@AuthenticationPrincipal CurrentUser currentUser) {

    // return the page view for list of attacks

    return "someview";
}

The currentUser here would reflect to the current user per say (Player 1 or 2 or 3 and so on). Which works fine for most of the stuff happening to themselves such as purchasing some stuff, updating profile and so on. But what I can't get or know how to achieve is when 2 players interact.

For example, Player 1 attacks Player 2. If I am Player 1, what I'll do is to click the "Attack" on the View and select the Player 2, and submit the command. Hence, in the controller, it will be something like this.

@GetMapping("/attack")
public String launchAttack(@AuthenticationPrincipal CurrentUser currentUser, @RequestParam("playername") String player2) {

    updatePlayerState(player2);

    return "someview";
}

public void updatePlayerState(String player) {

    User user = getUserByPlayername(player);
    // perform some update to player state (say health, etc)
    // update back to db?
}

Here's is what really got me confused. As seen previously, when each User/Player logs in, a set of user (player) current state will be pulled from the DB and store "in-memory". Hence, when Player 1 attacks Player 2,

  1. How do I "notify" or update Player 2 that the stats has changed, and thus, Player 2 should pull updated stats from db to memory.

  2. How to tackle the possible concurrency issue here? For example, Player 2 health is 50 in DB. Player 2 then perform some action (say purchase health potion + 30), which then update the DB (health to 80). However, just before the DB is updated, Player 1 has already launch the attack and grab from DB the state of Player 2 where it will return 50 since DB has yet to be updated. So now, whatever changes made in getUserByPlayername() and update to the DB will be wrong, and the entire state of the Player will be "de-sync". I hope I am making sense here.

I understand that there is @Version in hibernate for optimistic locking but I'm not sure if it's applicable in this case. And would spring-session be useful in such case?

Should I not store the any data in memory when user login? Should I always be retrieving data from DB only when some action is performed? Like when viewProfile, then I pull from accountRepository. or when viewStats then I pull from statsRepository and on so.

Do point me in the right direction. Would appreciate for any concrete example of sort, or some kind of video/articles. If there is any additional information required, do let me know and I'll try to explain my case better.

Thank you.

user1778855
  • 659
  • 1
  • 11
  • 29
  • You could start from [here](https://www.baeldung.com/websockets-spring) – Branislav Lazic Oct 27 '18 at 09:42
  • Thanks for the link, I do know about WS. I'm not exactly asking for client-server notification. It's more of how the server side (backend) can be notified between different players for the change of entity state as I mentioned in my example. – user1778855 Oct 27 '18 at 15:44
  • you definitely need to store data in memory but not in session, what you need is a caching mechanism which you can take control over state. invalidate cache > retrieve from DB > cache agian. I suggest infinispan/hazelcast or sth simpler like ehcache/jcache – Omid P Nov 04 '18 at 10:37
  • The data are expected to change frequently, so wouldn't it make no sense to cache those data. There are some that doesn't change much, so I could use caching for it but certainly not in the case of my question. Unless there's something that I missed out? – user1778855 Nov 04 '18 at 15:32

2 Answers2

2

I think that you should not be updating the currentUser in your Controller methods, and should not be relying on the data in that object to represent a player's current state. There are probably ways to get that to work, but you'd need to mess around with updating the security context.

I also recommend that you lookup Users by id instead of userName, so will write the rest of this answer with that approach. If you insist on finding Users by userName, adjust where necessary.

So, keeping it simple, I would have a reference to the accountRepository in the Controller, and then, whenever you need to get or update a player's state, use

User user = accountRepository.findById(currentUser.getId())

Yes, @Version and optimistic locking will help with the concurrency issues that you're concerned about. You can reload the Entity from the database, and retry the operation if you catch an @OptimisticLockException. Or, you may want to respond to player 1 with something like "Player 2 has just purchased a potion of healing, and is now 80 heath, do you still want to attack?"

Bernie
  • 2,253
  • 1
  • 16
  • 18
  • Thanks @Bernie. If you meant `getUserByPlayername(player)` to use `id` instead of `username` then you're right. I was just making an example to use username. Thanks for the idea, will take note of that for `@retry`. So whenever a user make any action, I should always make a DB call to fetch the latest info instead of storing "in-memory"? Would that stress out the IO part? – user1778855 Nov 01 '18 at 08:06
  • Yes, I recommend loading from the repository whenever you need the "up to date" information, or you want to change it. There's a potential performance issue because of the IO, but this can be mitigated with caching to a certain extent. If you have more than application server, then you'll need to be careful caching in the application server, as it can get out of sync with others in the cluster. If DB / IO really becomes a problem, then you might need to look into a product / library such as Hazelcast to keep a shared cache in sync. – Bernie Nov 01 '18 at 23:29
  • Alright, thanks for the tips. I would also like to check that it is alright for me to specify the 2 parameter as such in `launchAttack` right? That way, I would have the player that initiate the attack by getting the currentUser, and the player being attack by getting the playername param. I was wondering if there any better alternative to this approach as well. – user1778855 Nov 02 '18 at 17:57
  • There are potential security concerns there as you're taking the second player's identifier from the request without verifying that it hasn't been "hacked". It would be pretty simple for a player to spoof a request and send any player's username / ID and potentially attack a player that isn't even in their vicinity. How to prevent that kind of behaviour is worthy of a new question though. – Bernie Nov 04 '18 at 21:03
  • Yes, I agree that would totally be another question on its own. I will take that in mind while designing the app. Thanks alot. – user1778855 Nov 07 '18 at 14:24
1

I'm not a spring user, but I think that the problem is more conceptual than technical.
I'll try to provide an answer which uses a general approach, while writing the examples in a JavaEE style so that they should be understandable, and hopefully, portable to spring.

First of all: every single DETACHED entity is stale data. And stale data is not "trustable".

So:

  1. each method that modify the state of an object should re-fetch the object from DB inside the transaction:

    updatePlayerState() should be a transaction-boundary method (or called inside a tx), and getUserByPlayername(player) should fetch the target object from the DB.

    JPA speaking: em.merge() is forbidden (without proper locking, i.e. @Version).

    if you (or spring) are doing this already, there's little to add.
    WRT the "lost update problem" you mention in your 2. be aware that this covers the application server side (JPA/Hibernate), but the very same problem could be present on DB side, which should be properly configured for, at least, repeatable read isolation. Take a look at MySQL does not conform to Repeatable Read really, if you are using it.



  1. you have to handle controller fields that refer stale Players/Users/Objects. You have, at least, two options.

    1. re-fetch for each request: suppose Player1 has attacked Player2 and diminished Player2 HP by 30. When Player2 goes to a view that shows his HP, the controller behind that view should have re-fetched the Player2/User2 entity before rendering the view.

      In other words, all of your presentation (detached) entities should be, sort of, request-scoped.

      i.e you can use a @WebListener to reload your Player/User:

      @WebListener
      public class CurrentUserListener implements ServletRequestListener {
      
          @Override
          public void requestInitialized(ServletRequestEvent sre) {
              CurrentUser currentUser = getCurrentUser();
              currentUser.reload();
          }
      
          @Override
          public void requestDestroyed(ServletRequestEvent sre) {
              // nothing to do
          }
      
          public CurrentUser getCurrentUser() {
              // return the CurrentUser
          }
      }
      

      or a request-scoped bean (or whatever-spring-equivalent):

      @RequestScoped
      public class RefresherBean {
          @Inject
          private CurrentUser currentUser;
      
          @PostConstruct
          public void init()
          {
              currentUser.reload();
          }
      }
      
    2. notify other controller instances: if the update succeeded a notification should be sent to other controllers.

      i.e. using CDI @Observe (if you have CDI available):

      public class CurrentUser implements UserDetails {
      
          private static final long serialVersionUID = 1L;
          private User user = new User();
      
          public CurrentUser(User user) {
              this.user = user;
          }
      
          public PlayerStats getPlayerStats() {
              return this.user.getStats();
          }
      
          public void onUpdate(@Observes(during = TransactionPhase.AFTER_SUCCESS) User user) {
              if(this.user.getId() == user.getId()) {
                  this.user = user;
              }
          }
      
          // removed the rest for brevity 
      }
      

      Note that CurrentUser should be a server-managed object.

Michele Mariotti
  • 7,372
  • 5
  • 41
  • 73
  • For `RefresherBean object`, does that mean that for every HTTP request, the entities will be refreshed by using `.reload()` which calls DB to populate the latest data? The HTTP request refers to the individual player request right? Meaning to say, Player A click on a link which triggers the HTTP request, so it only reloads for Player A. Player B data will only be reloaded if and only if Player B also clicked on some link that triggers the HTTP request. Am I right to say so? `CurrentUser` refers to the `AuthenticationPrincipal`. – user1778855 Nov 07 '18 at 14:44
  • Exactly. Each request will reload only the (single) `User` entity relative to the "current" user. References to other Players/Users on other controllers should, instead, be loaded on-demand. – Michele Mariotti Nov 07 '18 at 14:56
  • Would there be any difference if the HTTP request is a Ajax request? If there anything needs to handle, or it doesn't matter since ajax is also a form of HTTP request? – user1778855 Nov 07 '18 at 15:00
  • Ajax requests are also HTTP requests. It doesn't matter. At worst, if you re-render just a fragment of the view, other parts of the view may contain obsolete info. – Michele Mariotti Nov 07 '18 at 18:21