Reconstruction de Hive dand HDP : patch, test et build

SCHOUKROUN Leo

By SCHOUKROUN Leo

6 oct. 2020

La distribution HDP d’Hortonworks va bientôt être dépreciée a profit de la distribution CDP proposée par Cloudera. Un client nous a demandé d’intégrer d’une nouvelle feature de Apache Hive sur son installation reposant sur HDP 2.6.0. Il s’agit d’une bonne opportunité pour essayer de compiler manuellement la version de Hive incluse dans HDP 2.6.0. Dans cet article, nous allons tenter de backporter une liste de patchs fournie par Hortonworks sur le project open source Hive de manière automatisée.

Re-build de Hive dans HDP 2.6.0

La première étape fut d’essayer de compiler la même version de Apache Hive que dans HDP 2.6.0.

Pour chaque release d’HDP, Hortonworks (désormais Cloudera) publie dans ses release notes la liste des patchs appliqués sur chaque composants. Nous allons d’abord récupérer cette liste appliqués sur Apache Hive 2.1.0 et la mettre dans un fichier texte :

cat hdp_2_6_0_0_patches.txt

HIVE-9941
HIVE-12492
HIVE-14214
...
...
HIVE-16319
HIVE-16323
HIVE-16325

Il y en a 120 :

wc -l hdp_2_6_0_0_patches.txt 
120 hdp_2_6_0_0_patches.txt

Comment appliquer ces patchs ?

Application des patchs dans l’ordre d’Hortonworks

Avant d’appliquer les patchs, nous pouvons cloner le repository d’Apache Hive et faire un checkout de la version incluse dans HDP 2.6.0 qui est la 2.1.0 :

git clone https://github.com/apache/hive.git
git checkout rel/release-2.1.0

Nous pensions au début que les patchs étaient listés par Hortonworks dans l’ordre chronologique de merge dans la branche master de Hive. Nous avons donc essayé de télécharger les patchs et de les appliquer dans cet ordre.

Pour chaque patch dans la liste, il faut récupérer le fichier patch dans la JIRA associée et lancer la commande git apply sur ce patch. Cela peut s’avérer fastidieux pour des dizaines de patchs. Nous verrons plus loin dans l’article comment automatiser ce processus.

Commençons par appliquer le premier patch de notre liste : HIVE-9941. Nous pouvons constater dans l’issue JIRA qu’il y a plusieurs pièces jointes.

HIVE-9941 attachments

Téléchargeons la dernière et essayons de l’appliquer :

wget https://issues.apache.org/jira/secure/attachment/12833864/HIVE-9941.3.patch

git apply HIVE-9941.3.patch 
HIVE-9941.3.patch:17: new blank line at EOF.
+
HIVE-9941.3.patch:42: new blank line at EOF.
+
HIVE-9941.3.patch:71: new blank line at EOF.
+
HIVE-9941.3.patch:88: new blank line at EOF.
+
warning: 4 lines add whitespace errors.

La commande git apply renvoie quelques avertissements mais le patch a bien été appliqué comme nous l’indique la commande git status :

git status
HEAD detached at rel/release-2.1.0
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	HIVE-9941.3.patch
	ql/src/test/queries/clientnegative/authorization_alter_drop_ptn.q
	ql/src/test/queries/clientnegative/authorization_export_ptn.q
	ql/src/test/queries/clientnegative/authorization_import_ptn.q
	ql/src/test/queries/clientnegative/authorization_truncate_2.q
	ql/src/test/results/clientnegative/authorization_alter_drop_ptn.q.out
	ql/src/test/results/clientnegative/authorization_export_ptn.q.out
	ql/src/test/results/clientnegative/authorization_import_ptn.q.out
	ql/src/test/results/clientnegative/authorization_truncate_2.q.out

nothing added to commit but untracked files present (use "git add" to track)

Ce patch a créer des nouveaux fichiers mais n’en a modifié aucun.

Continuons avec le prochain patch de la liste, HIVE-12492 :

wget https://issues.apache.org/jira/secure/attachment/12854164/HIVE-12492.02.patch

git apply HIVE-12492.02.patch
HIVE-12492.02.patch:362: trailing whitespace.
        Map 1 
[...]
error: src/java/org/apache/hadoop/hive/conf/HiveConf.java: No such file or directory
error: src/test/resources/testconfiguration.properties: No such file or directory
error: src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java: No such file or directory
error: src/java/org/apache/hadoop/hive/ql/optimizer/stats/annotation/StatsRulesProcFactory.java: No such file or directory
error: src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java: No such file or directory

Cette fois-ci, nous rencontrons plusieurs erreurs du type No such file or directory. Sur la première ligne du fichier patch, nous pouvons voir que le fichier ciblé est common/src/java/org/apache/hadoop/hive/conf/HiveConf.java :

diff --git common/src/java/org/apache/hadoop/hive/conf/HiveConf.java common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index 3777fa9..08422d5 100644
--- common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -1422,6 +1422,11 @@ private static void populateLlapDaemonVarsSet(Set<String> llapDaemonVarsSetLocal
         "This controls how many partitions can be scanned for each partitioned table.\n" +
[...]

L’erreur nous indique que le fichier src/java/org/apache/hadoop/hive/conf/HiveConf.java n’existe pas. C’est parce que la commande git apply dispose d’une propiété -p (décrite ici) qui enlève la première partie des chemins des fichiers (la valeur par défaut est 1) :

Nous n’avons pas rencontré cette erreur avec le précédent patch car ce-dernier utilisait un préfixe dans le chemin des fichiers, par exemple :

diff --git a/ql/src/test/queries/clientnegative/authorization_alter_drop_ptn.q b/ql/src/test/queries/clientnegative/authorization_alter_drop_ptn.q

Réessayons avec -p0 :

git apply -p0 HIVE-12492.02.patch

HIVE-12492.02.patch:362: trailing whitespace.
        Map 1 
[...]
error: patch failed: common/src/java/org/apache/hadoop/hive/conf/HiveConf.java:1422
error: common/src/java/org/apache/hadoop/hive/conf/HiveConf.java: patch does not apply
error: patch failed: itests/src/test/resources/testconfiguration.properties:501
error: itests/src/test/resources/testconfiguration.properties: patch does not apply
error: patch failed: ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java:53
error: ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java: patch does not apply
error: patch failed: ql/src/java/org/apache/hadoop/hive/ql/optimizer/stats/annotation/StatsRulesProcFactory.java:51
error: ql/src/java/org/apache/hadoop/hive/ql/optimizer/stats/annotation/StatsRulesProcFactory.java: patch does not apply

C’est mieux mais le patch ne s’applique toujours pas. Cette fois-ci nous rencontrons l’erreur patch does not apply qui est assez parlante.

En reprenant la liste des JIRA fournie par Hortonworks, nous nous sommes rendu compte que les patchs étaient donnés par ordre numérique. Prenons le cas des deux issues suivantes : HIVE-14405 and HIVE-14432. La première est plus ancienne que la deuxième mais cette dernière a bénéficiée d’un patch avant l’autre. Si ces deux patchs étaient ammenés à modifier le même fichier, nous pourrions rentrontrer l’erreur ci-dessus.

Appliquer les patchs dans cet ordre ne semble pas être la bonne solution. Dans la prochaine partie, nous allons essayer d’appliquer les patchs dans l’ordre dans lequel ils ont été push sur la branche master.

Application des patchs par ordre d’upload sur JIRA

Essayons d’itérer sur la liste patch pour voir combien nous pouvons en appliquer avec cette nouvelle stratégie.

Récupérer et appliquer tous les patchs depuis JIRA peut être fastidieux pour les raisons suivantes :

  • Il y en a 120
  • Nous venons de constater que certains patchs sont appliqués au niveau d’arborescence -p0 et d’autres au niveau -p1
  • Certains JIRA indiqués par Hortonworks n’ont aucune pièce jointe (ex : HIVE-16238)
  • Certains JIRA ont des pièces jointes qui ne sont pas des patchs (ex : HIVE-16323 dans laquelle il y a une capture d’écran)
  • Certains patchs sont au format .txt au lieu de .patch (ex : HIVE-15991)

Voici un extrait (en CoffeeScript) du script que nous avons écrit pour récupérer le dernier patch de chaque JIRA :

generate_patch_manifest = ->
  # For each patch in the given list, return a manifest
  patches_manifest = hdp_2_6_0_0_patches
  # Generate a manifest with patch id, description, creation date and url
  .map (patch) ->
    # Parse the JIRA id
    jira_id = patch.split(/:(.+)/)[0]
    # Parse the commit description
    commit_description = patch.split(/:(.+)/)[1]
    # Get all the attachments
    response = await axios.get "https://issues.apache.org/jira/rest/api/2/search?jql=key=#{jira_id}&fields=attachment"
    # Filter the attachments that are actual patches
    attachments = response.data.issues[0].fields.attachment.filter (o) -> o.filename.includes jira_id
    # Get only the latest patch
    most_recent_created_time = Math.max.apply Math, attachments.map (o) -> Date.parse o.created
    most_recent_created_attachment = attachments.filter (o) -> Date.parse(o.created) is most_recent_created_time
    # Create the patch manifest (implicit return in CoffeeScript)
    id: jira_id
    description: commit_description
    created: Date.parse most_recent_created_attachment[0].created
    url: most_recent_created_attachment[0].content
  # Order the patches by date
  .sort (a,b) ->
    a.created - b.created
  # Write tbe object to a YAML file
  fs.writeFile './patch_manifest.yml', yaml.safeDump(patches_manifest), (err) ->
    if err
    then console.error err 
    else console.info 'done'

Cette fonction génère un fichier patch_manifest.yaml avec la syntaxe suivante :

- id: $JIRA_NUMBER
  description: $COMMIT_MESSAGE
  created: $TIMESTAMP
  url: $LATEST_PATCH_URL

Créer ce manifeste va nous aider à appliquer les patchs dans l’ordre.

Nous pouvons désormais itérer sur cette liste, télécharger et essayer d’appliquer le plus de patch possible. Voici un autre extrait de code réalise cela :

# For each patch in the manifest
for patch, index in patches_manifest
  try
    await nikita.execute """
    # Download the patch file
    wget -q -N #{patch.url}

    # If the patch as the "a/" leading part in the files, apply with -p1
    if grep "git \\\\ba/" patches/#{patch.url.split('/').pop()}; then
      git apply patches/#{patch.url.split('/').pop()}

    # If not, then apply with -p0
    else
      git apply -p0 patches/#{patch.url.split('/').pop()}
    fi

    # Commit changes
    git add -A
    git commit -m "Applied #{patch.url.split('/').pop()}"
    """
    console.info "Patch #{patch.url.split('/').pop()} applied successfully"
  catch err
    console.error "Patch #{patch.url.split('/').pop()} failed to apply"

Cela ne s’est pas très bien passé. Seulement 19 des 120 patchs dans la liste se sont appliqués correctement. Toutes les erreurs patch does not apply peuvent avoir plusieurs causes :

  1. Nous n’appliquons toujours pas les patchs dans le bon ordre
  2. Certains pré-requis au niveau du code sont manquants (la liste des JIRA serait incomplète ?)
  3. Le code doit être adapté pour être backporté
  4. Toutes les propositions précédentes

Dans la partie suivante, nous allons essayer une autre stratégie : réaliser un cherry-picking des commits liés aux JIRAs HIVE-XXXXX.

Application des patch dans l’ordre chronologique de push sur la branche master

Chaque message de commit du repository Apache Hive comprends le numéro de l’issue JIRA liée dans sa description :

Hive commits

Pour ce nouvel essai, nous allons récupérer la liste des commits de la branch master Apache Hive pour générer une liste de commits contenants les patchs que nous souhaitons appliquer dans l’ordre chronologique.

git checkout master
git log --pretty=format:'%at;%H;%s' > apache_hive_master_commits.csv

La commande git log avec les bons paramètres permet de générer un fichier CSV avec le format timestamp;commit_hash;commit_message.

Par exemple :

head apache_hive_master_commits.csv

1597212601;a430ac31441ad2d6a03dd24e3141f42c79e022f4;HIVE-23995:Don't set location for managed tables in case of replication (Aasha Medhi, reviewed by Pravin Kumar Sinha)
1597206125;ef47b9eb288dec6e6a014dd5e52accdf0ac3771f;HIVE-24030: Upgrade ORC to 1.5.10 (#1393)
1597144690;d4af3840f89408edea886a28ee4ae7d79a6f16f8;HIVE-24014: Need to delete DumpDirectoryCleanerTask (Arko Sharma, reviewed by Aasha Medhi)
1597135126;71c9af7b8ead470520a6c3a4848be9c67eb80f10;HIVE-24001: Don't cache MapWork in tez/ObjectCache during query-based compaction (Karen Coppage, reviewed by Marta Kuczora)
1597108110;af911c9fe0ff3bb49d80f6c0109d30f5e1046849;HIVE-23996: Remove unused line in UDFArgumentException (Guo Philipse reviewed by Peter Vary, Jesus Camacho Rodriguez)
1597107811;17ee33f4f4d8dc9437d1d3a47663a635d2e47b58;HIVE-23997: Some logs in ConstantPropagateProcFactory are not straightforward (Zhihua Deng, reviewed by Jesus Camacho Rodriguez)
1596833809;5d9a5cf5a36c1d704d2671eb57547ea50249f28b;HIVE-24011: Flaky test AsyncResponseHandlerTest ( Mustafa Iman via Ashutosh Chauhan)
1596762366;9fc8da6f32b68006c222ccfd038719fc89ff8550;HIVE-24004: Improve performance for filter hook for superuser path(Sam An, reviewed by Naveen Gangam)
1595466890;7293956fc2f13ccbe72825b67ce1b53dce536359;HIVE-23901: Overhead of Logger in ColumnStatsMerger damage the performance
1593631609;4457c3ec9360650be021ea84ed1d5d0f007d8308;HIVE-22934 Hive server interactive log counters to error stream ( Ramesh Kumar via Ashutosh Chauhan)

Les commandes suivantes permettent de récupérer uniquement les commits qui nous intéressent : la liste de JIRAs fournie par Hortonworks. Finalement, nous pouvons trier cette liste par ordre chronologique :

cat hdp_2_6_0_0_patches.txt | while read in; do grep $in apache_hive_master_commits.csv; done > commits_to_apply.csv
sort commits_to_apply.csv > sorted_commits_to_apply.csv

Maintenant que nous disposons d’une liste ordonnée de commits que nous voulons appliquer, nous pouvons revenir à la version 2.1.0 et cherry-pick ces commits.

Le cherry-pick semble être une solution plus adaptée que l’application de fichiers patchs. En effet, nous venons de constater que certains patchs n’étaient pas applicables à cause du manque de code dépendant.

Essayons de faire du cherry pick sur le premier JIRA. Le commit hash du HIVE-14214 est b28ec7fdd8317b47973c6c8f7cdfe805dc20a806.

head -1 sorted_commits_to_apply.csv
1469348583;b28ec7fdd8317b47973c6c8f7cdfe805dc20a806;HIVE-14214: ORC Schema Evolution and Predicate Push Down do not work together (no rows returned) (Matt McCline, reviewed by Prasanth Jayachandran/Owen O'Malley)

git cherry-pick -x 15dd7f19bc49ee7017fa5bdd65f9b4ec4dd019d2 --strategy-option theirs
CONFLICT (modify/delete): ql/src/test/results/clientpositive/tez/explainanalyze_5.q.out deleted in HEAD and modified in 15dd7f19bc... HIVE-15084: Flaky test: TestMiniTezCliDriver:explainanalyze_1, 2, 3, 4, 5 (Pengcheng Xiong). Version 15dd7f19bc... HIVE-15084: Flaky test: TestMiniTezCliDriver:explainanalyze_1, 2, 3, 4, 5 (Pengcheng Xiong) of ql/src/test/results/clientpositive/tez/explainanalyze_5.q.out left in tree.
[...]
CONFLICT (modify/delete): ql/src/test/queries/clientpositive/explainanalyze_1.q deleted in HEAD and modified in 15dd7f19bc... HIVE-15084: Flaky test: TestMiniTezCliDriver:explainanalyze_1, 2, 3, 4, 5 (Pengcheng Xiong). Version 15dd7f19bc... HIVE-15084: Flaky test: TestMiniTezCliDriver:explainanalyze_1, 2, 3, 4, 5 (Pengcheng Xiong) of ql/src/test/queries/clientpositive/explainanalyze_1.q left in tree.
error: could not apply 15dd7f19bc... HIVE-15084: Flaky test: TestMiniTezCliDriver:explainanalyze_1, 2, 3, 4, 5 (Pengcheng Xiong)

L’output ci-dessus a été raccourci mais le point important à retenir est que nous rencontrons des conflits. La commande git cherry-pick dans cette situation essaye de faire un git merge du commit que nous ciblons vers notre branche actuelle. L’intérêt du cherry-pick dans cette situation est que nous pouvons forcer le processus pour faire en sorte que notre fichier local tel qu’il est dans le commit que nous ciblons.

Pour cela, nous pouvons ajouter le paramètre --strategy-option theirs à la commande de cherry-pick. Celle-ci va corriger la plupart des confits mais nous pouvons encore en rencontrer du type :

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	deleted by us:   llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/SerDeEncodedDataReader.java
	added by them:   llap-server/src/java/org/apache/hadoop/hive/llap/io/encoded/VectorDeserializeOrcWriter.java

Ces-derniers devront être gérés manuelement avec git add ou git rm selon la situation.

Avec la méthode du cherry-pick et la résolution manuelle des conflits pour certains patchs, nous avons été en mesure d’importer les 120 patchs JIRA que nous avons ciblé en début d’article.

Remarque : Nous avons essayé d’automatiser le processus au maximum mais il est important de noter qu’il est fortement recommandé d’exécuter les tests unitaires avant de compiler et de déployer les patchs sur un cluster de production. Nous allons aborder ce sujet dans la prochaine partie.

git checkout rel/release-2.1.0

while read line; do
  commit=$(echo $line | awk -F "\"*;\"*" '{print $2}')
  if git cherry-pick -x $commit --strategy-option theirs > patches/logs/cherry_pick_$commit.out 2>&1; then
    echo "Cherry picking $commit SUCCEEDED"
  else
    git status > patches/logs/cherry_pick_$commit.out 2>&1
    git status | sed -n 's/.* by .*://p' | xargs git add
    git -c core.editor=true cherry-pick --continue
    echo "Cherry picking $commit SUCEEDED with complications"
  fi
done < sorted_commits_to_apply.csv

Avec le script ci-dessus, nous pouvons appliquer 100% des patchs. Dans la section suivante, nous allons voir comment compiler et tester notre distribution de Hive.

Compilation

Avant de lancer les tests unitaires, essayons déjà de faire un build de notre release Hive avec la commande suivante :

mvn clean package -Pdist -DskipTests

Après quelques minutes, le build s’interrompt avec l’erreur suivante :

[ERROR] /home/leo/Apache/hive/storage-api/src/test/org/apache/hadoop/hive/ql/exec/vector/TestStructColumnVector.java:[106,5] cannot find symbol
  symbol:   class Timestamp
  location: class org.apache.hadoop.hive.ql.exec.vector.TestStructColumnVector

Si on regarde notre version du fichier ./storage-api/src/test/org/apache/hadoop/hive/ql/exec/vector/TestStructColumnVector.java, la classe java.sql.Timestamp n’est en effet pas importée.

Ce fichier a été modifié par le cherry-pick du commit de HIVE-16245.

Ce fix modifie 3 fichiers :

  • ql/src/java/org/apache/hadoop/hive/ql/optimizer/physical/Vectorizer.java
  • storage-api/src/java/org/apache/hadoop/hive/ql/exec/vector/VectorizedRowBatch.java
  • storage-api/src/test/org/apache/hadoop/hive/ql/exec/vector/TestStructColumnVector.java

Comment Hortonworks s’y est pris pour appliquer ce patch tout en étant capable de compiler sans erreurs ? Dans la prochaine section, nous allons explorer le repository Hive public d’Hortonworks pour comprendre comment les patchs listés pour HDP 2.6.0.0 ont été appliqués.

Exploration du repo hive2-release de Hortonworks

Sur Github, Hortonworks (maintenant Cloudera) maintient un repository public pour chaque composant de la stack HDP. Celui qui nous intéresse est hive2-release :

git clone https://github.com/hortonworks/hive2-release.git
cd hive2-release && ls

README.md

De prime abord, le repository semble vide mais les fichiers peuvent être trouvés en faisant le checkout d’un tag. Il y a un tag par release d’HDP :

git checkout HDP-2.6.0.3-8-tag

Essayons de voir comment Cloudera a appliqué le patch qui a provoqué notre erreur de build précédente :

git log --pretty=format:'%at;%H;%s' | grep HIVE-16245
1489790314;2a9150604edfbc2f44eb57104b4be6ac3ed084c7;BUG-77383 backport HIVE-16245: Vectorization: Does not handle non-column key expressions in MERGEPARTIAL mode

Maintenant, regardons quels fichiers ont été modifiés par ce commit :

git diff 2a9150604edfbc2f44eb57104b4be6ac3ed084c7^ | grep diff

diff --git a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/physical/Vectorizer.java b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/physical/Vectorizer.java
diff --git a/storage-api/src/java/org/apache/hadoop/hive/ql/exec/vector/VectorizedRowBatch.java b/storage-api/src/java/org/apache/hadoop/hive/ql/exec/vector/VectorizedRowBatch.java

Contrairement à ce qui est dans le patch (et ce qui a été push sur la branche master d’Apache Hive), le backport effectué par Cloudera modifie seulement 2 fichiers sur 3. Le procédé semble donc être manuel et non documenté. Les fichiers ayant menés à l’erreur lors du build n’ont pas été modifiés ici.

Conclusion

Le backporting d’une liste de patchs sur un projet open source tel que Apache Hive est difficile à automatiser. Avec la distribution HDP et selon le type de patch, Hortonworks a appliqué des modifications manuelles sur le code source ou bien a travaillé sur une version “forkée” de hive. Une bonne compréhension de la structure globale du project (modules, test unitaires, etc.) et de son écosystème (dépendances) est également nécéssaire pour pouvoir réaliser un build testé et fonctionnel.

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnées par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.