I have a Laravel application which uses a lot of AJAX POST and GET requests (Single Page Application). Once an item is saved via POST, a GET request is sent to reload parts of the page and get any new data.
After enabling split read and write database connections using the Laravel connection configuration, the application runs incredibly quickly (never thought this would be a problem!). It saves and then requests so quickly that the RO database (reporting just 22ms behind) doesn't get chance to update and I end up with old information.
I have enabled the sticky parameter in the database configuration which I thought would mitigate the problem, but the POST and GET requests are separate so the stickiness gets lost.
I could rewrite a large portion of the application POST requests respond with the correct data, but this doesn't work for reloading many components at once and is an enormous job so I see this as a last resort.
Another idea I had was to modify the getReadPdo(){...} method and $recordsModified value inside the Database Connection class so that the stickiness is saved on the user's session for up-to 1 second. I was unsure if this would cause any further issues with speed or excessive session loading that it would cause more problems.
Has anyone experienced this before or have any ideas on how to tackle the problem?
Thanks in advance.
Thought I'd update and answer this in case anyone else came across the same issue.
This isn't a perfect solution but has worked well over the last week or so.
Inside the AppServiceProvider boot() method, I added the following
DB::listen(function ($query) {
if (strpos($query->sql, 'select') !== FALSE) {
if (time() < session('force_pdo_write_until')) {
DB::connection()->recordsHaveBeenModified(true);
}
} else {
session(['force_pdo_write_until' => time() + 1]);
}
});
In a nutshell, this listens to every DB query. If the current query is a SELECT (DB read), we check to see if the "force_pdo_write_until" key inside the user session has a timestamp that is more than the current time. If it is, we trick the current DB connection into using the ReadPDO by utilizing the recordsHaveBeenModified() method - this is how the core Laravel sticky sessions are normally detected
If the current query is not a SELECT (most likely a DB Write), we set the session variable for "force_pdo_write_until" for 1 second in the future.
Any time a POST request is sent, if the next GET request is within 1 second of the previous query, we can be sure that the current user will be using the RW DB connection and get the correct results.
Update (09/12/19):
It turns out the solution above doesn't actually modify the DB connection at all, it was just adding a few milliseconds of processing time to any request so looked like it was working about 75% of the time (because the DB replica lag fluctuates depending on load).
In the end I decided I'd go a bit deeper and override the DB connection class directly and modify the relevant functions. My Laravel instances uses MySQL, so I overrode the Illuminate\Database\MySqlConnection class. This new class was registered through a new service provider, which in turn is loaded through the config.
I've copied the config and files I used below to make it easier for any new developers to understand. If you're copying these directly, make sure you also add the 'sticky_by_session' flag to your connection config as well.
config/database.php
'connections' => [
'mysql' => [
'sticky' => true,
'sticky_by_session' => true,
...
],
],
config/app.php
'providers' => [
App\Providers\DatabaseServiceProvider::class
...
],
app/Providers/DatabaseServiceProvider.php
<?php
namespace App\Providers;
use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
if (config('database.connections.mysql.sticky_by_session')) {
Connection::resolverFor('mysql', function ($connection, $database, $prefix, $config) {
return new MySqlConnection($connection, $database, $prefix, $config);
});
}
}
}
app/Database/MySqlConnection.php
<?php
namespace App\Database;
use Illuminate\Database\MySqlConnection as BaseMysqlConnection;
class MySqlConnection extends BaseMysqlConnection
{
public function recordsHaveBeenModified($value = true)
{
session(['force_pdo_write_until' => time() + 1]);
parent::recordsHaveBeenModified($value);
}
public function select($query, $bindings = [], $useReadPdo = true)
{
if (time() < session('force_pdo_write_until')) {
return parent::select($query, $bindings, false);
}
return parent::select($query, $bindings, $useReadPdo);
}
}
Inside of recordsHaveBeenModified(), we just add a session variable for later use. This method is used by the normal Laravel sticky session detection, as mentioned previously.
Inside of select(), we check to see if the session variable was set less than a second ago. If so, we manually force the request to use the RW connection, otherwise just continue as normal.
Now that we're directly modifying the request, I haven't seen any RO race conditions or effects from the replica lag.
I've published as a package!
mpyw/laravel-cached-database-stickiness: Guarantee database stickiness over the same user's consecutive requests
Installing
composer require mpyw/laravel-cached-database-stickiness
The default implementation is provided by ConnectionServiceProvider, however, package discovery is not available.
Be careful that you MUST register it in config/app.php by yourself.
<?php
return [
/* ... */
'providers' => [
/* ... */
Mpyw\LaravelCachedDatabaseStickiness\ConnectionServiceProvider::class,
/* ... */
],
/* ... */
];
Thats all! All problems will be solved.
Related
I'm looking for a way to save database entities changes for some entities. I mean I need to save in a database table all changes that are done on some tables (add, modify / delete) with ability to track user which did the change.
I'm working on NextJS with a custom ExpressJS server and MYSQL database were I use Prisma as ORM. I think it's maybe possible to write an ExpressJS middleware but I have yet no idea how to do it and asking myself if any library already exist.
Usually I work on PHP Symfony and used to manage this StofDoctrineExtensionsBundle which is great and works as expected. But my current project is a Typescript project only with Express/NextJS/React/Prisma/MYSQL.
Any feedback from your knowledge will be very appreciate.
Thank's in advance.
Regards,
Gulivert
EDIT: My current API which has to be moved to Express/NextJS is still running on Symfony and the table where all changes is logged looks like this :
{
"id": 59807,
"user": "ccba6ad2-0ae8-11ec-813f-0242c0a84005",
"patient": "84c3ef66-548a-11ea-8425-0242ac140002",
"action": "update",
"logged_at": "2021-11-02 17:55:09",
"object_id": "84c3ef66-548a-11ea-8425-0242ac140002",
"object_class": "App\\Entity\\Patient",
"version": 5,
"data": "a:2:{s:10:\"birth_name\";s:2:\"--\";s:10:\"profession\";s:2:\"--\";}",
"username": "johndoe",
"object_name": "patient",
"description": null
}
Explanation about database columns:
user => relation to user table
patient => relation to patient table
action => can be "create"/"update"/delete"
logged_at => date time where the change was done
object_id => entity row ID where an entity get a change
object_class => the entity updated
version => how many time the object was change
data => all data changed during the modification
username => the username of logged user did the change
object_name => a string to identify the object modified without
using the namespace of object_class
description => a value that can be update on some specific change * during usually the action delete to keep a trace what was deleted for instance
You might find prisma middleware useful for this.
Check out the example with session data middleware which is somewhat similar to what you're doing.
For your use-case the middleware might look like something like this:
const prisma = new PrismaClient()
const contextLanguage = 'en-us' // Session state
prisma.$use(async (params, next) => {
// you can find all possible params.action values in the `PrismaAction` type in `.prisma/client/index.d.ts`.
if (params.model == '_modelWhereChangeIsTracked_' && (params.action == 'create' || params.action == "update")) {
// business logic to create an entry into the change logging table using session data of the user.
}
return next(params)
})
// this will trigger the middleware
const create = await prisma._modelWhereChangeIsTracked_.create({
data: {
foo: "bar"
},
})
However, do note that there are some performance considerations when using Prisma middleware.
You can also create express middleware for the routes where you anticipate changes that need to be logged in the change table. Personally, I would prefer this approach in most cases, especially if the number of API routes where changes need to be logged is known in advance and limited in number.
I want to store in my database all the user actions done about an entity.
For example, for 1 entity, I want to store :
Created by (= author)
Updated by
Date of creation
Date of update
I want to store the history of the actions of a user, not the last ones. I thought I could create a table with these columns :
log_id
user_id
entity_id
action (= "create" or "update" or something else)
date
And then, I could easily get the last update of my entity and display the date and the user who did it.
Is there a Symfony bundle to do this ? Should I use Monolog ?
I will do this for many entities and I'm not sure if this is the correct way to do...
Is it possible to create only one logs table to store each log about each entity ? It bothers me to create 1 logs table per entity.
Since Doctrine is event based, it's easy:
Either use an extension, like Gedmo Loggable
Or hook into Doctrine's events and log, using Monolog, everything that happens in your app.
Personally I would prefer option 2 since I'm a control maniac, it's a little more complex though. Personally I would also use Monolog so I could abstract away the way how and where the log entries are stored.
When you decide how to approach this and you will need any assistance along the way, please ask another question.
Good luck.
I don't know if that would fit what you need, but you could easily add a Listener to the symfony kernel to log every controller used.
Something like this :
class UserLogListener {
protected $authChecker;
protected $tokenStorage;
protected $entityManager;
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationChecker $authChecker, EntityManager $entityManager)
{
$this->authChecker = $authChecker;
$this->tokenStorage = $tokenStorage;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
if( $this->tokenStorage->getToken() != null){
$user = $this->tokenStorage->getToken()->getUser();
$currentDate = new \Datetime();
$action = $event->getRequest()->attributes->get('_controller');
$method = $event->getRequest()->getMethod();
$userIp = $event->getRequest()->getClientIp();
$userLogRepository = $this->entityManager->getRepository(UserLog::class);
if($user instanceof User){
$userLog = new UserLog();
$userLog->setUser($user);
$userLog->setIp($userIp);
$userLog->setAction($action);
$userLog->setMethode($method);
$userLog->setDate($currentDate);
if($event->getRequest()->request && $methode=='POST'){
$userLog->setData(json_encode($event->getRequest()->request->all()));
}else{
$userLog->setData($event->getRequest()->getPathInfo());
}
$this->entityManager->persist($userLog);
$this->entityManager->flush();
}
}
}
}
What it does is add to the database (with an entity called UserLog) information about every page called. So you can know which action is made by knowing which controller is called, and you can also log the request data so you can find out what modification/creation the user did.
TL;DR How can I use my own way of generating the remember_me token?
I have an old site, written without any framework, and I have been given the job to rewrite it in Laravel (5.4.23). The DB is untouchable, cannot be refactored, cannot be modified in any way.
I was able to customise the Laravel authentication process using a different User model, one that reflect the old DB. But when it comes to the "Remember me" functionality, I have an issue with the length of the token.
The old site already uses the "Remember me" functionality but its DB field has been defined as BINARY(25). The token generated by the SessionGuard class is 60 characters long.
My first attempt was to try and find a way to shorten the token before writing it into the DB, and expand it again after reading it from the DB. I couldn't find such a way (and I'm not even sure there is such a way).
Then I looked into writing my own guard to override the cycleRememberToken (where the token is generated). I couldn't make it work, I think because the SessionGuard class is actually instantiated in a couple of places (as opposed to instantiate a class based on configuration).
So, I am stuck. I need a shorten token and I don't know how to get it.
Well, I was on the right track at one point.
I had to create my own guard, register it and use it. My problem, when I tried the first time, was that I did not register it in the right way. Anyway, this is what I did.
I put the following in AuthServiceProvides
Auth::extend('mysession', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new MyGuard('lrb', $provider, app()->make('session.store'));
$guard->setCookieJar($this->app['cookie']);
$guard->setDispatcher($this->app['events']);
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
return $guard;
});
I change the guard in config/auth.php as
'guards' => [
'web' => [
'driver' => 'mysession',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
and finally my new guard
class MyGuard extends SessionGuard implements StatefulGuard, SupportsBasicAuth
{
/**
* #inheritdoc
*/
protected function cycleRememberToken(AuthenticatableContract $user)
{
$user->setRememberToken($token = Str::random(25));
$this->provider->updateRememberToken($user, $token);
}
}
I have a very strange problem with my Symfony application. Everything is working fine on symfony 3.0.9, but when upgrading to 3.1 (currently running 3.1.3) I get the following error with almost all Controllers:
"Controller "Name_of_Controller::name_of_method" requires that you
provide a value for the "$request" argument (because there is no
default value or because there is a non optional argument after this
one)."
Here is an example of a method that causes this error:
/**
* This method handles add faculty
* requests
*
* #param Request $request html request
*
* #return Response html response
*
**/
public function addAction(Request $request)
{
// create a new Faculty
$faculty = new Faculty();
$faculty->setFirstname('Enter First Name');
$faculty->setLastname('Enter Last Name');
$form = $this->createForm(FacultyType::class, $faculty);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$task = $request->get('Submit');
// let see what the user wants
switch ($task) {
case 'Add':
// user wants to add
// we are setting the fullname of the object
$lname = $form->getData()->getLastname();
$fname = $form->getData()->getFirstname();
$faculty->setFullname("$lname, $fname");
$em = $this->getDoctrine()->getManager();
$em->persist($faculty);
$em->flush();
// report success
$this->addFlash('success', "The faculty member $faculty was successfully saved!");
return $this->redirectToRoute('faculty_index');
break;
case 'Cancel':
// report failure
$this->addFlash('failure', "The action was cancelled. No faculty member was saved!");
return $this->redirectToRoute('faculty_index');
}
}
return $this->render(
'faculty/add.html.twig',
[
'form' => $form->createView(),
]
);
}
The xml route for this method is as follows:
<route id="faculty_add" path="/add" methods="GET POST">
<default key="_controller">AppBundle:Faculty:add</default>
</route>
As mentioned above, it only happens on 3.1, the whole app works fine on 3.0.9.
Anybody else seen this? Do I have a structural problem in my code that was unveiled in 3.1 (I am not a professional coder...)
thanks!
Andreas
This is not a definitive answer, but might be helpful for those other hobby coders out there that have a similar problem:
The problem disappears when using php7.0 instead of 7.1. The same is true when running symfony 3.0.x on php 7.1.
It only persists when using both php 7.1 and symfony 3.1.
As I tried both dev and prod environments, emptied the cache repeatedly and could reproduce this both on a Digital Ocean Ubuntu droplet and my MacBook Pro, I can't imagine it is a caching issue.
Not sure whether this is a true bug or there is something in my code that causes this problem.
I'll update this should I find a more definitive cause for the problem.
Edited:
I have finally found the cause of the problem (for me): there was a typo in:
use Symfony\Component\HttpFoundation\Request;
(a lowercase letter instead of an uppercase letter). Strange thing: it had been working for years with all the Symfony/PHP versions (from Sf-2.6 and PHP-5.4) and it became a problem recently with the Sf/PHP versions mentioned below.
PHP namespaces are case-insensitive, but Symfony's resolvers seem to be sometimes case-sensitive...
Previous answer:
It seems that this behavior is due to the "ReflectionType improvements" feature in PHP 7.1: pull request.
The consequences on Symfony have been reported on this issue: github.com/symfony/symfony/issues/19677 (sorry, I can't create more than 2 links with my reputation...).
The feature has been reverted since then: ticket.
I followed the CakePHP Cookbook's simple ACL application tutorial and for a while all way fine and dandy. When I created a user, my AROs were automagically created too, and without too much effort I was able to give everyone permissions for the correct actions.
My application has become more complex now though. When I create a "Realtor", I create a user for them in the Realtor model's afterSave function, like so:
App::import( 'Component', 'Auth' );
$this->Auth = new AuthComponent();
$this->User->create();
$this->User->set(array(
'username' => $this->data['Realtor']['email'],
'password' => $this->Auth->password($this->data['Realtor']['password']),
'usergroup_id' => 2,
'realtor_num' => $this->id
));
if ($this->User->save()) {
$this->save(array('user_id'=>$this->User->id));
} else {
//error
}
Unfortunately, while this is successfully creating users, and the data all seems to match up with my expectations, I'm seemingly no longer getting AROs.
My Usergroup model contains the line
var $actsAs = array('Acl' => array('type' => 'requester'));
Beyond that, I have no idea how I would persuade my application to generate an ARO.
Is there anything I could have forgotten, that would help me get my ACL back on track?
EDIT:
I had this in the User model's afterSave which seems to have been causing various kinds of trouble:
function afterSave($created) {
if (!$created) {
$parent = $this->parentNode();
$parent = $this->node($parent);
$node = $this->node();
$aro = $node[0];
$aro['Aro']['parent_id'] = $parent[0]['Aro']['id'];
$this->Aro->save($aro);
}
}
(courtesy of this article: http://mark-story.com/posts/view/auth-and-acl-automatically-updating-user-aros) I don't know if that would have been fouling up my ARO creation somehow... probably teach me to add in random code snippets without fully understanding what they're doing, at the very least!
Ok I' m newbie
I can't understand your problem but
now I use Acl component with alaxos acl plugin
I try to understand of modified tree traversal algorithm
and set basic data for aro,aco,aro_aco,group table
and some requirement of plugin
I suggest you to use this