Articol disponibil AICI
08 noiembrie 2025
06 noiembrie 2025
Training Java performance - Ziua 3 (microservicii)
reteaua Google - sa nu depinda de alte cabluri; 6 x 9 availability pt Google Login
Sistem neperformant:
Throttling: ex. rate limiting
Queuing
Auto-scaling
Startup time: rau pe Java > mai rau Spring > si mai rau EJB
Conexiune persistenta Browser-BE: websocket/grpc/http2; pt multe request-uri/zi de la putini useri
Disintegrator - spargere cod mare in bucati mici
avantaje: decuplare, refolosire, izolare bug, versiune java la zi (CVE, Spring), testabil mai usor (pe bucati), scaleaza independent, deploy independent (faster cycle time)
Spring AI, Spring MCP
Integrator: motive sa nu spargi codul
latenta, cuplare stransa, microserviciile sunt scumpe; consistenta datelor (ACID in loc de eventual consistency)
https://www.infoq.com/news/2023/03/stop-cloud-zombies-qcon/
enum in Dtos pt REST compatibility: cand modifici/scoti din enum strici; daca faci rename pe un camp din Dto; camp din required in optional;
test care verifica ca nu ai facut breaking change pe contract;
subjson in json "metadata" pt alte adaugiri
Clientii v1 identificati prin header-ele Authorization, X-Api-Key, X-Client-Id, Traceparent (incl timp)
OpenTelemetry Java Agent- pus in JVM pt tracing (userv)
swagger-ts-generator
isaqb.org - certificare arhitect
Get id repetat - eroare 429 Too Many Requests -> use batch API
Request multiplexing - astepti sa vina mai multe req, le pui in asteptare (sincron) ~ batching
Netflix: grpc + graphQL
CQRS - segregare responsabilitati command / query
POST/PUT return data?
Caz RM creare programare in 2 pasi -> ret. patientId -> GET alte patientId -> afli date despre alti pacienti
202 Accepted (dupa validare request)
200 OK [Retry-after] - prea lung, nu e gata, dar totul e ok, ruleaza async; clientul face polling
200 OK {status: DONE}
302 Redirect - pt rezultate mari
SAU: serverul creeaza un topic la care clientul se aboneaza
SAU: clientul primeste un HTTP callback (webhook) pe care sa faca polling
SAU: websocket
Agregator - face batching
Proxy - ruteaza orbeste
API proxy - la intrare in system
Service proxy - la iesire, spre API externe
Gateway: caching, compresie date, rutare requests, load balancing, service registry, http access log, metrici, tracing,
agrega date din mai multe API-uri == NOK ca implica business logic in Gw
autentifica request-uri (cu ajutorul unui serviciu), fara autorizare
rate limiting
Connect timeout < Response timeout -- trebuie setate, altfel infinite
Lock.tryLock(timeout)
Eroare 500 + header Retry-After - daca e din cauza unui serviciu extern (nu 500 propriu-zis)
resilience4j
health probe: aliveness & readiness (capabil de trafic)
unalive: conexiuni agatate permanent, disc corupt, OOME, deadlock
LB poate evita o instanta daca e slow/are erori.
Inbox table = store then process
Outbox table pattern = store and fwd
- pune mesajul in SQL cu un status, apoi luat si trimis extern, actualizat status
Operatii idempotente = retryable: GET, DELETE, PUT (overwrite)
POST: idempotent daca la retry trimite acelasi idempotency key (poate fi timestamp la ms); sau serverul sa tina un hash(comanda), salvat intr-un map cu comenzile din ultimele 30 minute -> daca primeste aceeasi comanda va face reject
PUT cu IK (UUID)
server.tomcat.max-connections
server.tomcat.accept-count
server.tomcat.threads.max
Bulkhead = apeluri simultane per JVM
Rate limiter = apeluri intr-un interval de timp
Quartz - orchestrare timers prin DB
Compartimentalizare -> izolare serviciu picat
Cache - for latency or for data replication
App de baza are un SLA (99.99) -> e ok sa chemi alt API cu SLA mai mare (99.999)
Istio: construieste un mic scut in jurul unui serviciu (throttling etc - il face ca un gateway)
Strategii de deployment: canary (ex. FB release in Argentina), blue/green (gradually transfer traffic), deploy in staging pt testeri si clienti timp de _;
release notes, feature flags
shadow deploy=rutezi input din prod si verifici ca da la fel, in paralel cu versiunea veche
Ex. connection pool starvation:
@Transactional
method() {
synchronized(b) {
// call a slow api
}
}
Cache miss 99%: e prea mic, a expirat, nu este gasita cheia (lipseste hashCode, equals in clasa cheii)
Teste pe cache!! --> AgencyCacheTest
Cache hit prea mare: key collision? (chei trunchiate, sunt mereu gasite)
@TimeAspect
@Timed
MeterRegistry - raporteaza metrici
@Observed
Monolit (risc: cod complex) -> microservicii (risc: integrarile)
=>
Mockito -> WireMock (care nu reflecta realitatea din prod)
Monolit: multe unit teste; uS: multe teste de integrare
uS: mai multe bug-uri in yaml config decat in cod
@FeignClient - pt apel rest dintr-un uS la altul
Jeff Bezos manifesto for externalization (2002)
ZoomIt- timer (gong)
Surse:
video: https://www.youtube.com/watch?v=arUOVFUKt6s
drive: https://drive.google.com/open?id=1A2WIOb6tZvkzkHwDEAQSfFCKQAIG4imt&usp=drive_fs
https://victorrentea.ro/slides/
04 noiembrie 2025
Training Java performance - Ziua 2 (JPA)
SOLR/ES/Lucene - full text search
Vector search - next thing
Brian: paginare N rezultate, apoi N/2 , N/4 ... dar intr-o singura tranzactie
Query-uri dinamice:
1. concat string-uri: WHERE 1+1" (ca apoi sa poti concatena alte conditii, if criteria)
2. Query DieSL: JpaQuery
- Diesel genereaza entitatile (ex. QMaterial)
3. CriteriaAPI
Planul de executie uraste "OR"
- face cache la planul construit anterior
- query diferit: alt plan
Index: ideal e sa ai distributie buna pe date (nu prea multe valori repetitive)
AskTom - (Oracle) - intrebari tuning etc
@Id
@GeneratedValue(strategy = _ )
- SEQUENCE: trebuie creata; la rollback tranzactie nu se poate recupera (da impresia ca e un rand sters in table)
INCREMENT BY 50: tragi din baza 50 id-uri odata
- IDENTITY = auto-increment; ceri de fiecare data din baza
- PK violation daca inserezi id-uri 'cunoscute'
- UUID - greu de ghicit, poate fi generat de client; dar urat
- TABLE
@Embedded pe un obiect din entity -> muti anumite attribute din clasa (coloane) pt a-i scurta marimea
@Entity to Domain Model - vezi notele
@Transactional pe metoda - nu poti face face try/catch pe query
- interzise tranzactiile manuale
Write behind: JPA amana scrierile pana inainte de commit (flush)- pt a insera/update in batch-uri
em.flush() - grabeste flush, dar nu comite; crapa de la erori
+ un select ar putea grabi flush
Carte: "Release it!"
Entitate gasita cu find pe o metoda @Transactional:
.set...() face update (auto-save)
alt .set nu face / nu se mai potriveste equals
em.detach(obj); // scoate din urmarire
em.clear(); // elimina din context toate entitatile (detach la tot), goleste 1st level cache al lui Hibernate
flush vs clear:
- flush scrie tot ce avea de scris; util: inainte sa chemi un PL/SQL, Trigger sau native query.
- goleste fara sa scrie nimic
@DynamicUpdate - modifica coloana din entitate doar daca chiar se modifica
1st level cache = transaction-scoped; find se uita in cache
2nd level cache = intre request-uri diferite (optional @Cacheble pe @Entity sau query hint)
- util: primul select ia din baza, al doilea select identic intr-un TTL=5 min ia din cache
Persistence context
@Cache pe entity class
em.merge(entity); // update fara auto-save, fara @Transactional, cand construiesti cu new entitatea Venita de la client (nu mai faci find)
Merge = suprascriere entitate veche
select parinte, copii si apoi update
cascade
- DETACH ~ detaseaza copiii odata cu parintele
- REFRESH ~ git reset
- MERGE - copiii se ins/upd/del
@Transactional cand doar citesti din BD: lazy init (entitatile-copii ca lista nu sunt dispo pt un framework gen Jackson)
@OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true)
entitate parent.getChildren().remove(1) => UPDATE copil set parent null => cu orphanRemoval omoara copiii cu FK = null
( valabil la em.merge )
Cand un camp nu trebuie sa fie modificat pe un flux (ajuta cu merge side effects):
1) @Column(updatable = false) String createdBy; // eroare daca incerci sa modifici din cod
2) setCreatedBy - ignora/arunca eroare daca e deja setat campul
3) suprascrii cu valoarea care era initial in baza
Concurenta la modificare entitate:
Optimistic lock: te lasa sa scrii, cand salvezi iti da eroare daca cineva a salvat inaintea ta
@Version
private Long version; // sau LocalDateTime
// la em.merge(entity) se adauga o conditie suplimentara: AND version = 1 (cat era pe entity)
Pessimistic lock: pus lock in baza = intarzie a doua tranzactie pana comite prima
cu row lock: SELECT ... where version=7 FOR UPDATE [NO WAIT - la nivel de rand]
- face lock pe randurile returnate de SELECT
tx2 va astepta pana tx1 comite; = default in postgres/H2
adaugare coloane IN_EDIT_BY, IN_EDIT_SINCE cand materialul incepe sa fie editat de un user (lock pe linia din tabel)
Nu folosim synchronized in EJB
Monitorizare "connection acquisition time" = metrici - Grafana, Kibana, New Relic
@Transactional:
persist, flush nu duc la commitment daca apare exceptie in metoda tranzactata
- orice metoda chemata in thread-ul curent e parte din aceeasi tranzactie
- thread async: NU, si acopera exceptia; pt ca se pierde conexiunea JDBC care avea tranzactia pornita; nu merge nici daca pasezi EntityManager-ul de pe thread-ul gazda
Metoda @Transactional care cheama alta metoda @Transactional = e aceeasi tranzactie (parinte)
Propagation.REQUIRES_NEW ca sa fie distincta
TxType.REQUIRED = este default
throw checked exception intr-o metoda @Transactional nu impiedica commit-ul, doar unchecked (runtime) impiedica (!!!)
=> metoda @Transactional nu se recomanda cu throws! - faci try/catch/throw RuntimeException
@Transactional cu Propagation.REQUIRES_NEW pe o metoda-copil din acelasi serviciu = ca si cum nu are @Transactional
! nu se da API-call din @Transactional
Deadlock:
synchronized(x) din java + SELECT FOR UPDATE id=7 in parallel cu
SELECT FOR UPDATE id=7 + synchronized(x) din java
View materializat pe disc si actualizat periodic;
Rainbow brackets- plugin IntelliJ
DbUnit
Tuning tips:
- EXISTS (SELECT...) in loc de SELECT COUNT() sau id=(SELECT id...)
- Evita SELECT DISTINCT pe mai multe coloane = multe comparatii
- WHERE in loc de HAVING
- INNER JOIN ON <cond> in loc de WHERE<cond>
- LIMIT
- UNION ALL nu UNION (=DISTINCT)
- A UNION B in loc de WHERE... OR..
- GROUP BY in loc de OVER PARTITION BY
- tabele auxiliare
- IN si = in loc de != si <> si NOT IN
25 octombrie 2025
Training Java performance - Ziua 1 (JPA)
Orice @Entity trebuie sa aiba @Id (Integer, Long sau String - cheie naturala, ideal)
@OneToMany(mappedBy="numele companiei in cealalta clasa")
@Inheritance(strategy=TABLE_PER_CLASS, SINGLE_TABLE, JOINED)
Exemplu: 2 tabele Pisica/miaunat & Caine/latrat
SINGLE_TABLE: simplu, unele coloane sunt degeaba, nu poti pune NOT NULL pe coloanele specific; se foloseste daca dif dintre clase sunt minime; @DiscriminatorValue & @DiscriminatorColumn
TABLE_PER_CLASS (2 tabele): nu poti face select pe Animal; faci UNION (scump)
JOINED (3 tabele): join la orice query (ca sa incluzi si Animal) ---> nu se foloseste in practica
PARTITION BY - sparge datele in datafile separate (dupa an sau luna de obicei);
se face dupa cum vei face cautarea
daca cauti dupa altceva, merge rau
- nu ocupa spatiu in plus, precum indecsii
INDEX e ceva complementar (~ hashMap); prea multi afecteaza performanta
Employee si EmployeeDetails - pot avea shared primary key (FK al lui Details sa fie si PK)
- datele masive pot fi tinute in alt loc daca nu sunt frecventate des
Evitare referinte reciproce Employee si EmployeeDetails - depinde de caz - daca selectezi details si vrei si employee-ul initial, pui
altfel @OneToOne pt details, in Employee; daca modifici unul poti uita de celalalt
@ManyToMany
@JoinTable
@Enumerated - cand campul este un enum
ProjectType { LIB("L") } - in db ajunge "L", nu 0 sau LIB
XML/JSON - salvat ca CLOB (character large object)
- editare xml din clob risca stricaciuni
- poti selecta bucati din XML cu instr speciale Oracle
- poate fi prea mare, ocupa discul -- OOME (out of memory error)
Trebuie citit/scris in BD via fisier (cu InputStream/OutputStream)
Best practice: nu incarcat in BD, ci arhivat si salvat pe disc (ftp sau pe acelasi disc), cu referinta in BD;
like pe coloana CLOB: dureaza f mult pt ca CLOB nu e indexabil
https://github.com/victorrentea/jpa/tree/systematic25
run StartDatabase, JpaApplication
JpaPlayground - cu Lombok
fol. EntityManager in loc de Repository (EE)
em.persist(new Teacher());
@Transactional in Spring & Jakarta
Creare 2 entitati inrudite: trebuie legate both-way - daca uiti?
Relatiile bidirectionale sunt greu de intretinut.
Design mai bun: getter sa fie unmodifiable - blochezi get/add; faci o metoda de add in care legi dublu;
in cealalta clasa setter-ul e de tip default (nivel de package) - sa nu poti accesa set din afara pachetului
@OneToOne (cascade = CascadeType.ALL)
- TeacherDetails nu are ref la Teacher
CascadeType.ALL intre parinte si copiii exclusivi ai lui (creare, stergere automata)
campuri List vs Set
- ordonare cu @OrderBy("type ASC, value desc") pt List, sortare din query JPA
@OrderColumn(name = "INDEX") - salveaza in DB ordinea manual setata de user in UI
altfel ordinea nu e garantata
hashCode/equals pe:
- id?
- campuri?
- id + campuri? - situatie campuri identice si unul are id=null; ambele circula
=> nu impl hashCode/equals pe entitati, facem propriul equals; mai tricky la HashSet
@NotNull - previne sa inserezi prin java (Jakarta validation la persist)
nullable = false - previne sa inserezi "pe sub mana" -- folosit in POC, nu in proiecte mature (nu creezi DB din java); pot ramane out of sync
@AssertTrue pe metoda ~ @NotNull cand validarea e doar pe campul unui copil
@OneToMany poate fi def unidirectional, cu @JoinColumn(name = "loanerId")
constructor in entity:
protected cu constructor gol - ca sa nu planga Hibernate
public cu parametri - este cel relevant
getter/setters: nu neaparat necesare, Hibernate foloseste reflectie pt a popula campurile
--> BalanceEntityListener ??
--> @Transient in BalanceEntity?
http://annas-archive.org/ -- book "sql performance explained"
URL BD: jdbc:mssql ----> jdbc:p6spy:mssql , cu username, parola, dependency la p6spy
-> alternativa mai buna la show-sql=true
@OneToMany
N+1 queries = 1 pt parinte, N pt copii (loading lazy info)
fetch = FetchType.LAZY by default (doar pe colectii); la fel si pt @ManyToMany
EAGER daca eviti N+1 queries -> incarca tot dinainte - a nu se folosi aproape niciodata
@BatchSize(size=20) - este Hibernate specific - incarca entitati inrudite in calupuri
Ia toate info odata (pt @ManyToOne)
select p from Person p
LEFT JOIN FETCH p.children
LEFT JOIN FETCH p.country ---- nu este colectie
@ManyToOne - si asta aduce N+1 queries
fetch = EAGER by default
entityManager.createQuery("SELECT pFROM errorProne"); // app porneste, vezi bug-ul tarziu
@NamedQuery (name = "", query = "") ; // detecteaza devreme, nu compileaza
Sub-select: select doar anumite campuri
@Subselect("query...")
public record.... - in spring
@NamedNativeQuery - pt EE
(query= "", resultClass = , resultSetMapping = )
- cu COALESCE
- to VIEW ----> declari un nou @Entity in Java cu @Table("VIEW_NAME")
Teste pe query-uri
FetchGraph - sa incarci partial entitati
Paginare: ORDER BY, LIMIT __, OFFSET __
- in BD, in Java sau pe client
problema: el face select tot dar iti da numai ce ceri
Nu poti pagina in DB daca faci FETCH, pt ca se strica cardinalitatea
pe doua directii: iese produs cartezian
LEFT JOIN FETCH ALL PROPERTIES - ia in cascada
@ManyToOne private Country country; ==> private Long countryId; // pastreaza FK
-> pune numeric/string ref in loc de object reference
entityManager
.createNamedQuery(..)
.setFirstResult(0) // offset
.setMaxResults(2) // limit
.getResultList();
@ElementCollection -- entitati-copii fara id
Cross join: N x N x N results
Intrerupere query in timpul rularii - exista api de jdbc "abort" - nu din JPA, doar JDBC (statement.cancel())
sau: timeout in JPA
13 septembrie 2025
Jupiter Parameterized Test
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
public class ServerStreamingInputValidationTest extends AbstractTest {
@ParameterizedTest
@MethodSource("testData")
void testBlockingInputValidation(WithdrawRequest request, Status.Code code) {
var ex = Assertions.assertThrows(StatusRuntimeException.class, () -> this.blockingStub.withdraw(request).hasNext());
Assertions.assertEquals(code, ex.getStatus().getCode());
}
@ParameterizedTest
@MethodSource("testData")
void testAsyncInputValidation(WithdrawRequest request, Status.Code code) {
var observer = new ResponseObserver<Money>();
this.asyncStub.withdraw(request, observer);
observer.await();
Assertions.assertTrue(observer.getItems().isEmpty());
Assertions.assertNotNull(observer.getThrowable());
Assertions.assertEquals(code, ((StatusRuntimeException) observer.getThrowable()).getStatus().getCode());
}
private Stream<Arguments> testData() {
return Stream.of(
// input, expectation
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(11).setAmount(10).build(), Status.Code.INVALID_ARGUMENT),
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(1).setAmount(17).build(), Status.Code.INVALID_ARGUMENT),
Arguments.of(WithdrawRequest.newBuilder().setAccountNumber(1).setAmount(120).build(), Status.Code.FAILED_PRECONDITION)
);
}
}
07 august 2025
Kill process in Windows & Linux
Windows:
> netstat -ano | findstr :8080
> taskkill PID <pid> /F
Linux:
> sudo lsof -i 8080
> sudo kill -9 <pid>
02 august 2025
Proto vs Json performance test
public class PerformanceTest {
private static final Logger LOGGER = LoggerFactory.getLogger(PerformanceTest.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
var protoPerson = Person.newBuilder()
.setLastName("T")
.setAge(32)
.setEmail("t@email.com")
.setEmployed(true)
.setSalary(123004.5)
.setBankAccountNumber(38495959093040944L)
.setBalance(-100)
.build();
LOGGER.info("how many bytes protoPerson has? {}", protoPerson.toByteArray().length); // 41
var jsonPerson = new JsonPerson("T", 32, "t@email.com", true, 123004.5, 38495959093040944L, -100);
var bytes = MAPPER.writeValueAsBytes(jsonPerson);
LOGGER.info("how many bytes jsonPerson has? {}", bytes.length); // 130 = 3x more!
for (int i=0; i<5; i++) { // first run to be ignored (warmup JVM)
runTest("json", () -> json(jsonPerson));
runTest("proto", () -> proto(protoPerson)); // 7x faster!
}
}
private static void runTest(String testName, Runnable runnable) {
var start = System.currentTimeMillis();
for (int i=0; i<5_000_000; i++) {
runnable.run();
}
var end = System.currentTimeMillis();
LOGGER.info("time taken for {} = {} ms", testName, (end - start));
}
private static void proto(Person person) {
try {
var bytes = person.toByteArray();
Person.parseFrom(bytes);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}
private static void json(JsonPerson person) {
try {
var bytes = MAPPER.writeValueAsBytes(person);
MAPPER.readValue(bytes, JsonPerson.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
